Object Interactions

Summary:
  • Timers and Events can control movement and change in objects.
  • Objects can respond to their spatial relationship to each other.
  • Objects can respond to mouse actions by the user.
GuiEvent is the base class for events in 2D space. SceneEvent is the base class for events in 3D space, which are the events of interest when you are using Scene.
IntersectionSceneEvent is the base class for events that involve spatial relationships among objects in the scene. All IntersectionSceneEvents have a 3D shape. This shape is determined by the property IntersectionSceneEvent.intersection-shape. The five basic shapes provided are:
For example, when you use a SphereIntersectionSceneEvent in a scene, you create an event that has a spherical shape and a specified center and radius. Where and how that spherical event intersects with other objects in the scene determines what objects respond to the event. Object-to-object interaction is further defined by IntersectionSceneEvent.intersection-style. See Intersection Style.
Events that involve a SceneObject responding to the mouse pointer are implemented as subclasses of RayIntersectionSceneEvent, which in turn is a subclass of IntersectionSceneEvent. Pointer events are essentially ray events fired into the scene at whatever object is under the pointer.

Movement and Animation

You can use Timers and Events to implement action in a Scene. See the Animation chapter. Another resource for animation is the TransformController, a subclass of SceneObjectController that implements a keyframe animation system with linear interpolation. See SceneObjectController and Animation
The following example uses a Timer to rotate the triangle around the z axis. Animation is controlled by the CommandButton at the bottom of the SceneGraphic.

Example: Scene Animation
{import * from CURL.GRAPHICS.SCENE}
{import * from CURL.PGUIDE.SCENEUTILS,
    location = "../../default/support/SceneUtils.scurl"}
{value
    let scene:ExampleScene = {ExampleScene}
    {scene.camera-setup}
    let scene-graphic:SceneGraphic =
        {SceneGraphic
            scene,
            width=4in,
            height=4in,
            background = {FillPattern.get-black}
        }
    let quad:Quad =
        {Quad
            0in, 0in, z = 1ft,
            4ft, 4ft
        }
    {scene.add-object quad}
    let tri:Triangle =
        {Triangle
            {Distance3d -1ft, 0ft, 1ft},
            {Distance3d -1ft, -5ft, 1ft},
            {Distance3d -1ft, 0ft, 4ft}
        }
    {scene.add-object tri}
    {scene.add-object {make-axis-object 1m}}
    let tmr:Timer=
    {scene-graphic.animate
        interval=.05s,
        repeat = 0,
        {on TimerEvent do
            {tri.rotate {Fraction3d 0, 0, 1}, -5deg}
            {scene-graphic.update-drawable}
        }
    }
    let run-stop-button:CommandButton =
    {CommandButton label = "Start",
        {on Action at b:CommandButton do
                {if tmr.repeat == 0 then
                    set tmr.repeat = -1
                    set b.label = "Stop"
                 else
                    set tmr.repeat = 0
                    set b.label = "Start"
                }
        }
    }
    let reset-camera:CommandButton =
        {CommandButton
            label = "Reset Camera",
            {on Action at b:CommandButton do
                {scene.camera-setup}
                {scene-graphic.update-drawable}
            }
        }
    {VBox
        scene-graphic,
        {HBox
            run-stop-button,
            reset-camera
        }
    }
}

Object Interactions with Other Objects

To create objects that interact in a scene, you need to take the following steps:
The following example illustrates an interaction between the Triangle and the Quad. When the Triangle intersects the Quad, the Quad changes color.

Example: Triangle and Quad Interaction
{import * from CURL.GRAPHICS.SCENE}
{import * from CURL.PGUIDE.SCENEUTILS,
    location = "../../default/support/SceneUtils.scurl"}
{define-class public TriangleCollisionSceneEvent
  {inherits BoxIntersectionSceneEvent}
  {constructor public {default ...}
    {construct-super
        intersection-style = IntersectionStyle.contained,
        predicate = Predicate.all,
        sort-order = SortOrder.none,
        ...
    }
  }
}
{value
    let scene:ExampleScene = {ExampleScene}
    {scene.camera-setup}
    let scene-graphic:SceneGraphic =
        {SceneGraphic
            scene,
            width=4in,
            height=4in,
            background = {FillPattern.get-black}
        }
    let eh-collision:EventHandler =
        {on e:TriangleCollisionSceneEvent at q:Quad do
            set q.fill-pattern = {FillPattern.get-yellow}
        }
    let quad:Quad =
        {Quad
            0in, 0in, z = 1ft,
            4ft, 4ft,
            fill-pattern = {FillPattern.get-gray},
            eh-collision
        }
    {scene.add-object quad}
    let tri:Triangle =
        {Triangle
            {Distance3d -1ft, 0ft, 1ft},
            {Distance3d -1ft, -5ft, 1ft},
            {Distance3d -1ft, 0ft, 4ft}
        }
    {scene.add-object tri}
    {scene.add-object {make-axis-object 1m}}
    let tmr:Timer=
    {scene-graphic.animate
        interval=.05s,
        repeat = 0,
        {on TimerEvent do
            {tri.rotate {Fraction3d 0, 0, 1}, -5deg}
            let a:Distance3d
            let b:Distance3d
            set (a, b) = {tri.get-parent-bounding-box}
            let tce:TriangleCollisionSceneEvent =
                {TriangleCollisionSceneEvent a, b}
            set quad.fill-pattern = {FillPattern.get-gray}
            {scene.handle-event tce}
            {scene-graphic.update-drawable}
        }
    }
    let run-stop-button:CommandButton =
    {CommandButton label = "Start",
        {on Action at b:CommandButton do
                {if tmr.repeat == 0 then
                    set tmr.repeat = -1
                    set b.label = "Stop"
                 else
                    set tmr.repeat = 0
                    set b.label = "Start"
                }
        }
    }
    let reset-camera:CommandButton =
        {CommandButton
            label = "Reset Camera",
            {on Action at b:CommandButton do
                {scene.camera-setup}
                {scene-graphic.update-drawable}
            }
        }
    {VBox
        scene-graphic,
        {HBox
            run-stop-button,
            reset-camera
        }
    }
}

Using sort-order and predicate

The parameters IntersectionSceneEvent.predicate and IntersectionSceneEvent.sort-order define how IntersectionSceneEvents move through a scene.
In the following example, when you click on a triangle, the event handler responds to that PointerPressSceneEvent and turns the triangle white. The event handler then creates a MultiPointIntersection containing all the intersections with RayEvent (a subclass of RayIntersectionSceneEvent). RayEvent travels from the intersection point in the positive x direction. Each triangle processed by the event handler turns a lighter shade of gray. Note that if predicate is set to first, only one triangle changes color. Note also that if sort-order is set to backward, the order in which the triangles are processed is reversed, although the order in which the triangles are intersected is the same.

Example: Using sort-order and predicate
{import * from CURL.GRAPHICS.SCENE}
{import * from CURL.PGUIDE.SCENEUTILS,
    location = "../../default/support/SceneUtils.scurl"}

{define-class RayEvent {inherits RayIntersectionSceneEvent}
  {constructor {default ...}
    {construct-super ... }}}
{value
    let scene:ExampleScene = {ExampleScene}
    set scene.camera-position = {Distance3d 0ft, -2ft, 3ft}
    {scene.camera-setup}
    let scene-graphic:SceneGraphic =
        {SceneGraphic
            scene,
            width=4in,
            height=4in,
            background = {FillPattern.get-black}
        }
    let event-handler:EventHandler =
        {on event:PointerPressSceneEvent at tri:Triangle do
            set tri.fill-pattern = {Color.from-rgb 1, 1, 1}
            let intersection-point:Distance3d =
                ((event.intersection asa
                  PointIntersection).intersection-point + {Distance3d .5in, 0in, 0in})
            let mpi:MultiPointIntersection =
                {scene.get-intersections
                    {RayEvent
                        intersection-point,
                        {Direction3d 1, 0, 0},
                        predicate = Predicate.all,
||                            predicate = Predicate.first,
||                            sort-order = SortOrder.none
||                            sort-order = SortOrder.backward
                        sort-order = SortOrder.forward
                    }
                }
            let clr-fract:Fraction = 0.0
            {for p:PointIntersection in mpi.intersections do
                {if p.object isa Triangle and p.object != tri then
                    set clr-fract = clr-fract + .1
                    set (p.object asa Triangle).fill-pattern =
                        {Color.from-rgb clr-fract, clr-fract, clr-fract}
                }
            }
        }
    {for x:Distance = -2.5ft to 2.5ft step 1ft do
        let vertex:Distance3d = {Distance3d x, 0in, 0in}
        {scene.add-object
            {Triangle
                vertex,
                vertex + {Distance3d .5ft, 0in, .2ft},
                vertex + {Distance3d 0in, .5ft, .2ft},
                event-handler
            }
        }
    }
    {scene.add-object {make-axis-object 1m}}
let reset-colors:CommandButton =
    {CommandButton
        label = "Reset Colors",
        {on Action at b:CommandButton do
            let mpi1:MultiPointIntersection =
                {scene.get-intersections
                    {RayEvent
                        {Distance3d -3ft, .1ft, .1ft},
                        {Direction3d 1, 0, 0},
                        predicate = Predicate.all,
                        sort-order = SortOrder.forward
                    }
                }
            {for p:PointIntersection in mpi1.intersections do
                {if p.object isa Triangle then
                    set (p.object asa Triangle).fill-pattern =
                        {FillPattern.get-magenta}
                }
            }
            {scene-graphic.update-drawable}
        }
    }

    let reset-camera:CommandButton =
        {CommandButton
            label = "Reset Camera",
            {on Action at b:CommandButton do
                {scene.camera-setup}
                {scene-graphic.update-drawable}
            }
        }
    {VBox
        scene-graphic,
        {HBox
            reset-camera,
            reset-colors
        }
    }
}

Intersection Style

Intersection style determines which objects respond to volumetric events. See IntersectionStyle for a list of the eight intersection styles available.
The following example tiles the x-y plane with gray Quads. A Quad turns yellow when it handles a BoundarySceneEvent. BoundarySceneEvent is a subclass of SphereIntersectionSceneEvent. The circle enables you to visualize where BoundarySceneEvents are taking place. The center and diameter of the circle are the same as the center and diameter of the BoundarySceneEvent where it intersects the x-y plane. You generate BoundarySceneEvents when you press one of the buttons to select IntersectionStyle, or when you move the circle with the mouse pointer.
This example has some additional features worthy of note. It implements BoundarySceneGraphic, a subclass of SceneGraphic. This subclass overrides the default method SceneGraphic.handle-motion, so that left mouse pointer motion moves the circle and right mouse pointer motion resizes the circle.

Example: Intersection Style
{import * from CURL.GRAPHICS.SCENE}
{import * from CURL.PGUIDE.SCENEUTILS,
    location = "../../default/support/SceneUtils.scurl"}

{define-class public BoundarySceneGraphic {inherits SceneGraphic}
  field public circle:Circle
  field public intersection-style:IntersectionStyle = IntersectionStyle.contained

  {constructor public {default scene:Scene, circle:Circle, ...}
    {construct-super scene, ... }
    set self.circle = circle
  }

  {method public
    {handle-motion
        button-pressed:int,
        dx:Distance,
        dy:Distance}:void
    {if button-pressed == 1 then
        let (sx:double, sy:double, sz:double) =
            {self.circle.transformation.to-world-scale}
        let s:double = 20 / {max sx, sy, sz}
        {self.circle.translate dx*s, -dy*s, 0m}
     elseif button-pressed == 3 then
        let s:float = (1 + dx/1in) asa float
        {self.circle.scale s, s, s}
    }
    {for obj:SceneObject in self.scene.objects do
        {if (obj isa Quad) then
            set (obj asa Quad).fill-pattern = {FillPattern.get-silver}
        }
    }
    {self.scene.handle-event
        {BoundarySceneEvent
            {self.circle.transformed-center},
            {self.circle.transformed-radius},
            self.intersection-style}
    }
    {self.update-drawable}
  }
  {method public {reset-quads}:void
    {for obj:SceneObject in self.scene.objects do
        {if (obj isa Quad) then
            set (obj asa Quad).fill-pattern =
                {FillPattern.get-silver}
        }
    }
    {self.update-drawable}
  }
}
{define-proc {make-command-button
                 label:String,
                 bsg:BoundarySceneGraphic,
                 intstyle:IntersectionStyle,
                 circle:Circle,
                 scene:Scene}:CommandButton
    let button:CommandButton =
        {CommandButton label=label,
            width = 1in,
            {on Action do
                {bsg.reset-quads}
                set bsg.intersection-style = intstyle
                {scene.handle-event
                    {BoundarySceneEvent
                        {circle.transformed-center},
                        {circle.transformed-radius},
                        intstyle}
                }
            }
        }
    {return button}
}
{define-class public BoundarySceneEvent {inherits SphereIntersectionSceneEvent}
  {constructor public
    {default
        center:Distance3d,
        radius:Distance,
        intersection-style:IntersectionStyle, ...}
    {construct-super
        center,
        radius,
        intersection-style = intersection-style,
        predicate = Predicate.all,
        sort-order = SortOrder.none,
        ...
    }
  }
}
{define-class public Circle {inherits SceneObject}
  field public center:Distance3d = {Distance3d 0m, 0m, 0m}
  field public radius:Distance
  field public color:Color = {Palette.get-cyan}

  {method public {paint renderer:Renderer3d,
                     viewport-width:Distance,
                     viewport-height:Distance}:void
    let center:Distance3d = self.center
    let radius:Distance = self.radius
    {renderer.render-elliptic-path
        center.x - radius, center.y - radius, 0m,
        radius * 2, radius * 2,
        texture=self.color, line-width = 1.5pt}
  }
  {method public
    {get-local-bounding-box
        check-visibility?:bool = false
    }:(min-xyz:Distance3d, max-xyz:Distance3d, valid-bounds?:bool)
    let center:Distance3d = self.center
    let radius:Distance = self.radius
    let a:Distance3d = {Distance3d center.x - radius, center.y - radius, 0m}
    let b:Distance3d = {Distance3d center.x + radius, center.y + radius, 0m}
    {return a, b, not check-visibility? or self.bounding-box-or-object-visible?}
  }
  {constructor public {default center:Distance3d, radius:Distance, ...}
    {construct-super ... }
    set self.center = center
    set self.radius = radius
  }
  {method public {transformed-center}:Distance3d
    {return {self.world-transformation.point-to-world self.center}}
  }
  {method public {transformed-radius}:Distance
    let (sx:double, sy:double, sz:double) =
        {self.world-transformation.to-world-scale}
    {return self.radius * {max sx, sy, sz}}
  }
}
{value
let scene:ExampleScene = {ExampleScene}
set scene.camera-position = {Distance3d 0in, -3ft, 5ft}
{scene.camera-setup}
let circle:Circle =
    {Circle
        {Distance3d 0m, 0m, 0m},
        3.1ft
    }
{scene.add-object circle}
let bsg:BoundarySceneGraphic =
    {BoundarySceneGraphic
        scene,
        circle,
        width=4in,
        height=4in,
        background = {FillPattern.get-black}
    }

let eh:EventHandler =
    {on e:BoundarySceneEvent at obj:Quad do
        set obj.fill-pattern = {FillPattern.get-yellow}
        {bsg.update-drawable}
    }
let quad:#Quad
{for x:Distance = -5ft to 5ft step 1ft do
    {for y:Distance = -5ft to 5ft step 1ft do
        set quad =
            {Quad
                0ft, 0ft,
                6in, 6in,
                fill-pattern = {FillPattern.get-silver},
                eh
            }
        {quad.translate x, y, 0m}
        {scene.add-object {non-null quad}}
    }
}
{VBox bsg,
    {HBox
        {make-command-button "always", bsg, IntersectionStyle.always, circle, scene},
        {make-command-button "entirely-contained", bsg, IntersectionStyle.entirely-contained, circle, scene},
        {make-command-button "contained", bsg, IntersectionStyle.contained, circle, scene},
        {make-command-button "not-on-border", bsg, IntersectionStyle.not-on-border, circle, scene}
    },
    {HBox
        {make-command-button "never", bsg, IntersectionStyle.never, circle, scene},
        {make-command-button "outside", bsg, IntersectionStyle.outside, circle, scene},
        {make-command-button "entirely-outside", bsg, IntersectionStyle.entirely-outside, circle, scene},
        {make-command-button "only-on-border", bsg, IntersectionStyle.only-on-border, circle, scene}
    }}}

Using opaque-to-intersection?

The property SceneObject.opaque-to-intersection? controls whether objects in a Scene can block other objects from receiving intersection events. The default value is true, which enables an object to get in the way of another object receiving intersection events. Set this property to false to enable events to pass through the object to intersect other objects.
The following example is based on the earlier example Adding a Triangle and a Quad to a SceneGroup. This example adds a blue rectangle. Initially, opaque-to-intersection? is set to false, which makes the blue rectangle transparent to intersection events. Note that PointerPressSceneEvents reach the gray rectangle even if you actually click on the blue one. Comment out the line that sets opaque-to-intersection? to revert to the default value of true. Execute the modified example. Note that when you click on the blue rectangle, the SceneGroup does not rotate. The blue rectangle is blocking PointerPressSceneEvents from reaching the gray rectangle.

Example: Using opaque-to-intersection?
{import * from CURL.GRAPHICS.SCENE}
{import * from CURL.PGUIDE.SCENEUTILS,
    location = "../../default/support/SceneUtils.scurl"}
{paragraph Click on the triangle to rotate clockwise}
{paragraph Click on the rectangle to rotate counter clockwise}
{value
    let scene:ExampleScene = {ExampleScene}
    {scene.camera-setup}
    let scene-graphic:SceneGraphic =
        {SceneGraphic
            scene,
            width=4in,
            height=4in,
            background = {FillPattern.get-black}
        }
    let quad:Quad =
        {Quad
            0in, 0in, z = 1ft,
            4ft, 4ft,
            {on PointerPressSceneEvent at obj:Quad do
                {obj.parent.rotate {Fraction3d 0, 0, 1}, 10deg}
            }
        }
    let cover-quad:Quad =
        {Quad
            0in, 0in, z = 1.1ft,
            3ft, 2ft,
            fill-pattern = {FillPattern.get-blue}
        }
    ||  Comment following line to start blocking intersection events
    set cover-quad.opaque-to-intersection? = false

    let tri:Triangle =
        {Triangle
            {Distance3d -1ft, 0ft, 1ft},
            {Distance3d -1ft, -5ft, 1ft},
            {Distance3d -1ft, 0ft, 4ft},
            {on PointerPressSceneEvent at obj:Triangle do
                {obj.parent.rotate {Fraction3d 0, 0, 1}, -10deg}
            }
        }

    {scene.add-object {SceneGroup quad, tri}}
    {scene.add-object cover-quad}
    {scene.add-object {make-axis-object 1m}}
    let reset-camera:CommandButton =
        {CommandButton
            label = "Reset Camera",
            {on Action at b:CommandButton do
                {scene.camera-setup}
                {scene-graphic.update-drawable}
            }
        }

    {VBox
        scene-graphic,
        reset-camera
    }
}

Object Interactions with the User

Objects in a scene can respond to mouse events generated by the user. SceneGraphic passes the necessary information about 2D mouse events to the method Scene.handle-pointer-event. Scene.handle-pointer-event converts the 2D event into an appropriate 3D event. There are five types of SceneEvents generated in response to the mouse pointer. All are subclasses of RayIntersectionSceneEvent.
You can get information about the pointer action from these pointer events. PointerPressSceneEvent and PointerReleaseSceneEvent, both provide these accessors:
PointerMotionSceneEvent, PointerEnterSceneEvent, and PointerLeaveSceneEvent provide only state-mask information. Pointer event handling in Scene is similar to pointer event handling in the GUI Toolkit. See Pointer Button Events and GUI Window and Input Events
The Quad in the following example rotates around a different axis depending on which mouse button you press, and turns a different color for each button if you also hold down the SHIFT key.

Example: Using pointer events in a scene
{import * from CURL.GRAPHICS.SCENE}
{import * from CURL.PGUIDE.SCENEUTILS,
    location = "../../default/support/SceneUtils.scurl"}
{value
    let scene:ExampleScene = {ExampleScene}
    {scene.camera-setup}
    let scene-graphic:SceneGraphic =
        {SceneGraphic
            scene,
            width=4in,
            height=4in,
            background = {FillPattern.get-black}
        }
    let quad:Quad =
        {Quad
            0in, 0in, z = 1ft,
            4ft, 4ft,
            {on e:PointerPressSceneEvent at obj:Quad do
                {if e.state-mask.shift? then
                    {if e.button == left-button then
                        set obj.fill-pattern = {FillPattern.get-red}
                    }
                    {if e.button == middle-button then
                        set obj.fill-pattern = {FillPattern.get-orange}
                    }
                    {if e.button == right-button then
                        set obj.fill-pattern = {FillPattern.get-yellow}
                    }
                 else

                    {if e.button == left-button then
                        {obj.rotate {Fraction3d 0, 0, 1}, 10deg}
                    }
                    {if e.button == middle-button then
                        {obj.rotate {Fraction3d 0, 1, 0}, 10deg}
                    }
                    {if e.button == right-button then
                        {obj.rotate {Fraction3d 1, 0, 0}, 10deg}
                    }
                }
            }
        }
    {scene.add-object quad}
    {scene.add-object {make-axis-object 1m}}
    let reset-camera:CommandButton =
        {CommandButton
            label = "Reset Camera",
            {on Action at b:CommandButton do
                {scene.camera-setup}
                {scene-graphic.update-drawable}
            }
        }
    {VBox
        {text font-weight="bold", Click on the Quad!},
        {text Left mouse: rotation around z axis},
        {text Middle mouse: rotation around y axis},
        {text Right mouse: rotation around x axis},
        {text Shift-Left mouse: red},
        {text Shift-Middle mouse: orange},
        {text Shift-Right mouse: yellow},
        scene-graphic,
        reset-camera
    }
}