Continuing the discussion from Waiting for Sketchup.send_action to complete before continuing:
And here is the working example with no ViewObserver
… no dedicated scene pages, … restoring the previous view, … no actual modification of the model. (You can close an unmodified model after writing images with no save prompt messagebox.)
This example is SketchUp 2015+, as it uses UI.select_directory
method.
(But I did get it to work for SU2014 when I did not need to select a directory, ie, opened a model previously saved that has a @model.path
. IF it is tried on SU2014 on an unsaved model a NoMethodError
will result.)
save_view_as_png_no_observer_demo.rb (11.8 KB)
Example uses the concepts of azimuth and elevation (as in locating the position of a satellite):
The file looks like this:
# encoding: UTF-8
#
# An example of saving the standard iso views to image files.
# This example does not use (or need) a ViewObserver class.
#
# It teaches:
# * Creating a bounding box, and iterating the model's
# entities to add drawingelement object bounds to it.
# * Use of transformations to locate the camera eye point
# and up vectors, ie, azimuth and elevation.
# * Saving and restoring the view camera.
# * Saving layer visibility states, hiding layers whose name
# contians a certain string, and restoring the previous
# layer visibility states.
# * Writing out the image files.
# * Creating a context menu of image write commands.
#
# This example carries NO WARRANTY. It is example code only.
# No copyright implied nor intended.
#
# Change top level module name to something unique for use.
# Submodule may be renamed to satisfy personal desires.
#
module Author; end
module Author::WriteViewImagesNoObserver
extend self
VERSION ||= '1.0.0'
EXT ||= 'png'
COMPANY ||= 'CompanyName'
# Used for Run Once block condition (bottom of module):
@@loaded ||= false
# Module variables that can be optionally changed:
@@iso_azimuth ||= Hash[ :NE,45, :NW,315, :SE,135, :SW,225 ]
@@iso_elevation ||= 20
@@iso_viewpoint ||= :SE
@@cline_bounds ||= false # include finite clines in image bounds?
@@hide_layers ||= true
@@hide_toggle ||= MF_CHECKED
def camera_bounds(clines= @@cline_bounds)
# Start with a new empty bounding box:
bb = Geom::BoundingBox::new
# Iterate the model entities collection:
@model.entities.each {|e|
# Skip object unless it's a Drawingelement subclass object.
# If so, entity will respond to .bounds(), .layer() and .visible?()
next unless e.is_a?(Sketchup::Drawingelement)
# Deal with Construction Lines:
if e.is_a?(Sketchup::ConstructionLine)
next if !clines
# Always avoid infinite clines !
next if ( e.start.nil? || e.end.nil? )
end
# Add the entity bounds to our bounding box,
# if it's visible and on a visible layer:
bb.add(e.bounds) if e.visible? && e.layer.visible?
}
bb # return the bounding box object
end
def camera_iso_setup( corner )
#
# Setup the Iso camera eye and target points, and up vector.
# Note, in SketchUp the Y axis points North (by default.)
#
# Get bounding box of visible entities:
bb = camera_bounds()
target = bb.center
# Temporarily locate the eye point North of the entity
# bounds center (target), on the same plane, at a distance of
# the bounds depth:
eye = target.transform([ 0, bb.depth, 0 ])
# Temporarily create the camera up vector as a clone of Z_AXIS:
up = Z_AXIS.clone
# Rotate eye point @@iso_elevation degrees above target plane,
# about a vector pointing from target, parallel to the X axis.
# Imagine that the rotatinal axis vector of the clock hands is
# pointing out away from the clock face, and you face the clock.
# Clockwise will be negative. Anti-clockwise positive.
t1 = Geom::Transformation::rotation(
target, X_AXIS, @@iso_elevation.degrees
)
eye.transform!(t1)
# Transform the up vector so it remains perpendicular to the
# eye -> target directional vector:
up.transform!(t1)
# Now, transform the elevated eye point at North position,
# @@iso_azimuth[corner] degrees (clockwise) to the proper
# azimuth, about the reversed Z axis of the target point.
# We must use a reverse vector because we want to rotate in
# clockwise direction but keeping the angle positive so that
# the rotational degrees match the standard compass degrees.
# If we did not reverse the Z vector (and the clock face,)
# positive angles would rotate the eye point opposite that of
# of a standard compass bearing. (We could also just negate
# the 3rd argument of the :rotation class method and use the
# Z_AXIS as is, pointing upward.)
t2 = Geom::Transformation::rotation(
target, Z_AXIS.reverse, @@iso_azimuth[corner].degrees
)
eye.transform!(t2)
# Transform the up vector to match:
up.transform!(t2)
# Return the camera properties:
[ eye, target, up ]
end
def camera_top_setup()
# Get bounding box of visible entities:
bb = camera_bounds()
# The target will be the center of the entities:
target = bb.center
# The eye will be directly above the target
# by a distance equal to the bounds height:
eye = target.transform([ 0, 0, bb.height ])
# The camera up vector will always point North:
up = Y_AXIS.clone
# Return the camera properties:
[ eye, target, up ]
end
def camera_setup( viewname, viewpt )
if viewname.to_s.downcase == 'top'
camera_top_setup()
elsif viewname.to_s.downcase == 'iso'
camera_iso_setup(viewpt)
else
puts "ERROR: #<#{Module::nesting[0].name}:camera_setup():"<<
" Unknown view name argument (\"#{viewname.to_s}\").>"
end
end
def get_filename( viewarg, viewpt )
path = File.dirname(@model.path)
# Use downcase() to create new string object
# so the original is NOT changed. Ie, Ruby
# passes arguments by reference, NOT by value.
# The first argument object is used unchanged
# for other subsequent method calls.
viewname = viewarg.to_s.downcase
if viewname == 'iso'
viewname << "_" << viewpt.to_s.downcase
end
if path.empty?
filename = "UNTITLED_#{viewname}.#{EXT}"
else
filename = "#{@model.title}_#{viewname}.#{EXT}"
end
[ path, filename ]
end
def get_filepath( path, filename )
# Returns nil if user cancels the dialog.
@filepath = UI.savepanel(
'Choose file save location ...',
path,
filename
)
end
def hide_layers()
# Remember layer visibility states, then hide non-company layers.
#
# Hash to remember previous layer states:
@layers = {}
# Remember the current active layer:
@active = @model.active_layer
layer_set = @model.layers
# For safety sake, we'll switch to "Layer0":
@model.active_layer= layer_set['Layer0']
# Iterate the model layers collection:
layer_set.each {|layer|
# Save each layer visibility state in the @layers hash:
@layers[layer.name]= layer.visible?
# Switch off non-company layers ...
# BUT, "Layer0" must always be visible!
next if layer.name == 'Layer0'
# Now, hide layers whose name does not contain company substring:
layer.visible= false if layer.name !~ /#{COMPANY}/
}
end
def hide_layers_toggle()
@@hide_layers = !@@hide_layers
@@hide_toggle =( @@hide_layers ? MF_CHECKED : MF_UNCHECKED )
end
def restore_layers()
layer_set = @model.layers
# Iterate the saved layer states in the @layers hash:
@layers.each {|name,state|
# Restore each layer visibility state:
layer_set[name].visible= state
}
# Restore the previous active layer:
@model.active_layer= @active
end
def remember_view()
# Reference the camera object for the active view:
cam = @model.active_view.camera
# Create a clone using the active camera properties:
@camera = Sketchup::Camera::new(
cam.eye, cam.target, cam.up, cam.perspective?, cam.fov
)
end
def restore_view()
# Set the model active view to use the cloned @camera:
@model.active_view.camera= @camera
end
def set_view( viewname, viewpt )
# The original view camera should already be cloned
# to the @camera reference. See: remember_view()
view = @model.active_view
# Set the camera:
cam = view.camera
eye, target, up = camera_setup(viewname,viewpt)
cam.set( eye, target, up )
cam.perspective=( viewname.to_s.downcase == 'iso' )
# Zoom to visible entities:
view.zoom(@model.entities)
end
def save_all_iso_views()
# Set a reference to the active model (used by many methods.)
@model = Sketchup.active_model
if @model.path.empty?
# Prompt for a directory:
savepath = UI.select_directory(
title: '',
directory: '',
select_multiple: false
)
if !savepath || savepath.empty? # empty String or Array
puts "#{Module::nesting[0].name}: User cancelled directory panel."
puts "Save all Iso view images aborted."
return false
end
else
savepath = File.dirname(@model.path)
end
# 1 time before loop:
puts "\nSave all Iso view images to:\n \"#{savepath}\""
viewname = 'iso'
remember_view()
hide_layers() if @@hide_layers
# For each of the azimuth viewpoints:
for viewpt in @@iso_azimuth.keys()
filename = get_filename(viewname,viewpt).last
@filepath = File.join(savepath,filename)
puts "Writing file: #{filename}"
# Set the view:
set_view(viewname,viewpt)
# Write the view to image file:
write_view_file()
end
# 1 time after loop, Restore the original view and layers:
restore_view()
restore_layers() if @@hide_layers
puts "Finsihed writing all iso view files."
end
def save_view( viewname, viewpt = @@iso_viewpoint )
# Set a reference to the active model (used by many methods.)
@model = Sketchup.active_model
path, filename = get_filename(viewname,viewpt)
if get_filepath(path,filename) # nil if user cancelled
remember_view()
hide_layers() if @@hide_layers
# Set the view:
set_view(viewname,viewpt)
# Write the view to image file:
puts "Writing file: #{filename}"
write_view_file()
# Restore the previous view:
restore_view()
# Restore the previous layer states:
restore_layers() if @@hide_layers
puts "Finsihed writing view to image file."
else
puts "#{Module::nesting[0].name}: User cancelled file save panel."
puts "#{viewname.capitalize} View image file save aborted."
end
end
def write_view_file()
# Switch to the save directory and perform operations:
Dir::chdir(File.dirname(@filepath)) {
file = File.basename(@filepath)
# Delete image file if it already exists.
# An overwrite confirmation box may be displayed to the user, if
# SketchUp view#write_image() thinks that the file already exists.
if File.exist?(file)
File.delete(file)
sleep 2 # Let the OS catch up.
end
# Write the image file:
written = Sketchup.active_model.active_view.write_image({
:filename => @filepath,
:antialias => true,
:transparent => true
})
# When the block ends, Ruby switches back to previous directory.
}
rescue => error
puts "Error in write_view_file()"
puts error.inspect
end
unless @@loaded # Run this block ONCE:
#
UI.add_context_menu_handler {|popup|
submenu = popup.add_submenu('Save View Image')
submenu.add_item('Default Iso View') { save_view('iso',@@iso_viewpoint) }
submenu.add_item('Save All Iso Views') { save_all_iso_views() }
toggle = submenu.add_item('Hide Non-Company Layers') { hide_layers_toggle() }
submenu.set_validation_proc(toggle) { @@hide_toggle }
submenu.add_separator
submenu.add_item('NorthEast Iso View') { save_view('iso',:NE) }
submenu.add_item('NorthWest Iso View') { save_view('iso',:NW) }
submenu.add_item('SouthEast Iso View') { save_view('iso',:SE) }
submenu.add_item('SouthWest Iso View') { save_view('iso',:SW) }
submenu.add_separator
submenu.add_item('Save Top View') { save_view('top') }
}
#
@@loaded = true
#
end
end