Setter #view.camera= Testing with Second Transition Time Argument

I am attempting to test the #view.camera= setter that includes a second argument of a transition time. Instead of manually coding multiple camera objects, I used a multi-scene model. (I have been able to sucessfully sequence through the scenes with a Ruby script by using a UI timer with "Sketchup.send_action ‘pageNext:’ ")

I tried the following:

  view = Sketchup.active_model.active_view
  Sketchup.active_model.pages.each{|page|
    view.camera= page.camera, 2.0
  }

The code snippet above does not work, but it does end up at the last camera view, but there was no delays.

Question 1: I have noticed that changing the camera alone with “camera=” does not introduce a “view.refresh”. Shouldn’t passing it a second argument not only interpolate the camera over the transition time, but and update the view as well for each frame?

I tried the same coded and stubbed a simple FrameChangeObserver object. It still did not work. My next attempt was statement below, which causes a syntax error during script loading:

view.camera=( page.camera, 2.0 )

Apparently Ruby does not recognize parenthesis with a setter. According to StackOverflow:

Multiple arguments can be dealt with the “send” method:

view.send(:camera=, page.camera, 2.0 )

The send method statement above caused the following run time exception:
Error: #<ArgumentError: wrong number of arguments (2 for 1)>test.rb:4:in `camera=’

Question #2: What am I doing wrong?

It works. The loop through the Pages happens fast - in milliseconds. It does not wait for the Scene transition to finish before looping to the next Scene. So all you see is the last transition because each iteration of the loop replaces the previous transition.

A FrameChangeObsever is used to watch a Scene transition. All it can do is tell you what percent of the time you are between a transition from one page to another. It can be used to detect when the transition is complete, and then call a method to go to the next Scene, for example.

Note that each Page an have its own transition time, and there is a default transition time which an be accessed through the Model Options:

tt = Sketchup.active_model.options["PageOptions"]["TransitionTime"]
Sketchup.active_model.options["PageOptions"]["TransitionTime"] = 2.0

What is your goal?

I didn’t need to know the default transition time, nor the page specific ones. The goal was to demonstrate to myself “#view.camera=” with a second argument. It does not appear to work,

Ha I should have checked the docs for the method. Either the docs or the method is bugged. I was surprised to see the method had an optional 2nd argument. That is unusual for a “=” method.

view = Sketchup.active_model.active_view
  Sketchup.active_model.pages.each{|page|
    view_camera = page.camera, 2.0
    view.camera = view_camera
  }

works, as far as I can tell…

Thanks. It does accept an array argument. I tried:

view.camera= [page.camera, 2]

and that also worked, but it does not work with a simple loop re-assigning multiple cameras, as the loop immediately re-assigns the camera. The camera transition time does not introduce a delay in the “camera=” method call. I think I would have to use the UI timer with a delay to make it work with multiple cameras.

[quote=“BruceYoung, post:6, topic:14512”]
Thanks. It does accept an array argument. I tried:

view.camera= [page.camera, 2]

and that also worked, …[/quote]

Because under the hood, Ruby is implemented in C, and argument lists are passed as arrays.

Time to introduce you to the “splat” operator. It is the asterisk (*) when used in certain situations. (It looks like someone threw a tomato at the wall, and it went “Splat!”.)

args = [page.camera, 2]
view.camera= *args

or just plain:

view.camera= *[page.camera, 2]

When prepended to an array expression, the splat operator expands the result of the array expression, into a parameter list.

When used where a parameter list is expected, the Ruby interpreter will collect parameters into an array.
This is usually done in method definitions:

def somename( *args )
  raise(ArgumentError,"No args!") if args.empty?
  # process the args array of parameters
  #   and do something with them...
end

You misread the send docs. There are only two arguments, and the second one is an array of arguments.

view.send(:camera=, [ page.camera, 2.0 ])

… or get the method object and use it’s call method …

view.method(:camera=).call( page.camera, 2.0 )

… or use it’s [] alias …

view.method(:camera=)[ page.camera, 2.0 ]

:bulb:

Okay, with a multi-scene model, I tried the following code statements in the Ruby console:

pages = Sketchup.active_model.pages
view = Sketchup.active_model.active_view
view.camera= [ pages[1].camera, 2 ]

I had it set to the first scene, and “pages[1]” is the second scene. It worked correctly. The iteration loop I tried at the start of this topic was for multiple scenes, and it does not work because the code immediately re-assigns the camera. I was able to get multi-scene to work by re-assigning the camera in a “UI.start_timer” code block.

Filter by only pages checked as “used in animation” ?

I just used this in a test of mine as:

@pages = model.pages.select {|pg| page_used_in_animation?(pg) }

ADD: Post release of SketchUp 2018

With v 18 or higher, a custom method is no longer needed.
Animation flag setter and getter methods were added to the Sketchup::Page class:

@pages = model.pages.find_all {|pg| pg.include_in_animation? }

I’ve logged a API issue for this confusing method and suggested a new “baby sister” method that would handle multiple methods better, pass the arguments to it’s :camera= “big sister”, and then return a new camera reference.

If you’d like to try it out as a Ruby 2.x+ Refinement … (SketchUp 2014+) …

module Refined
  module View

    refine Sketchup::View do

      def camera_set_to(*args)
        case args.size
        when 0
          fail(ArgumentError,'Wrong number of arguments (0 for 1 or 2)',caller)
        when 1
          cam = self.camera=(args.first)
        else
          cam = self.camera=(args) # ignores unused arguments
        end
        self.camera # Always return a ref to the new camera!
      end

    end

  end
end

Usage is like this:

using Refined::View
# with the current page at pages[0] (first page)
view = Sketchup::active_model.active_view
pages = Sketchup::active_model.pages
cam1 = pages[-1].camera # last page's camera
cam2 = view.camera_set_to( cam1, 3.5 )

:geek:

Hey Bruce, for example-sake, I wrote up an observer example that also solves your page camera animation scenario:

CameraShow.rb (1.5 KB)

To use, just type at the console:

CameraShow.new

… or …

CameraShow.new(2.5)

… to specify the transition time.

Has the drawback that scene styles are not part of the animation as they are with normal page to page animation.

Here is an old test model (normally used for testing Scene Change DC functions, from a very old blog post.)
It can work well for testing this as it has Scene number labels on each side of the kiosk.

DC_JohnClemens.skp (1.8 MB)


The ruby code (in the "CameraShow.rb" file) looks like:

class CameraShow < Sketchup::ViewObserver

  def initialize(time=3.0)
    @time = time
    @model = Sketchup.active_model
    view = @model.active_view
    @pages = @model.pages
    if @pages[0].respond_to?(:include_in_animation?)
      @pages = @pages.find_all {|pg| pg.include_in_animation? }
    end
    # Save current camera properties:
    cam = view.camera
    @camold = Sketchup::Camera::new(
      cam.eye, cam.target, cam.up, cam.perspective?, cam.fov
    )
    # Set the first page's camera as the goal:
    @index = 0
    @goal = @pages[@index].camera
    # attach the observer to the view:
    view.add_observer(self)
    if camera_at_goal?(view.camera)
      # Start on last page
      view.camera= @pages[-1].camera
      view.camera=[@goal,@time]
    else
      # Start on first page
      view.camera=[@goal,@time]
    end
  end

  def camera_at_goal?(cam)
    cam.eye == @goal.eye &&
    cam.target == @goal.target &&
    cam.up == @goal.up &&
    cam.perspective? == @goal.perspective? &&
    cam.fov == @goal.fov
  end

  def next_cam(view)
    @index += 1
    if @pages[@index]
      @goal = @pages[@index].camera
      view.camera=[@goal,@time]
    else # at end, transition back to old camera:
      view.remove_observer(self)
      @goal = @camold
      unless camera_at_goal?(view.camera)
        view.camera=[@camold,@time]
      end
    end
  end

  def onViewChanged(view)
    #
    next_cam(view) if camera_at_goal?(view.camera)
    #
  end

end