home *** CD-ROM | disk | FTP | other *** search
/ Freelog 100 / FreelogNo100-NovembreDecembre2010.iso / Graphisme / GoogleSketchUp / GoogleSketchUpWFR.exe / GoogleSketchUp8.msi / SketchUpMeta.cab / webtextures_loader.rb.78D17A5F_0E0A_44D2_877D_2C56D45D16B7 < prev    next >
Encoding:
Text File  |  2010-08-26  |  28.9 KB  |  923 lines

  1. #!/usr/bin/ruby
  2. #
  3. # Copyright:: Copyright 2009 Google Inc.
  4. # License:: All Rights Reserved.
  5. # Original Author:: Scott Lininger (mailto:scottlininger@google.com)
  6. #
  7. # This file declares the WebTextures class that provides hooks for showing
  8. # a web dialog with UI to select a texture and push it down to SketchUp
  9. # for auto-texturing of selected faces.
  10. #
  11. #     WebTextures          Self-contained object for showing dialog.
  12. #
  13. require 'sketchup.rb'
  14. require 'LangHandler.rb'
  15.  
  16. # The WebTextures class. An instance of this class handles all of the
  17. # dialog display and callbacks for grabbing and applying textures from
  18. # the web. You'll find the code that creates the instance at the
  19. # bottom of this file.
  20. #
  21. class WebTextures
  22.  
  23.   # Define some constants.
  24.   WT_DIALOG_REGISTRY_KEY = 'WebTextures'
  25.   WT_DIALOG_WIDTH = 400
  26.   WT_DIALOG_HEIGHT = 700
  27.   WT_DIALOG_MIN_WIDTH = 400
  28.   WT_DIALOG_MIN_HEIGHT = 360
  29.   WT_DIALOG_X = 10
  30.   WT_DIALOG_Y = 100
  31.   WT_DEFAULT_TEXTURE_WIDTH = 144
  32.   WT_VERY_LARGE_NUMBER = 999999
  33.   WT_REGISTRY_SECTION = "WebTextures"
  34.   WT_REGISTRY_KEY = "AgreedToEula"
  35.   WT_MAX_FACES_TO_PROCESS = 500
  36.  
  37.   # Creates a new WebTextures object.
  38.   #
  39.   #   Args:
  40.   #     title: string title of the WebDialog to display
  41.   #     strings: optional LanguageHandler object containing translated strings
  42.   #
  43.   #   Returns:
  44.   #     Nothing
  45.   #
  46.   def initialize(title, strings = LanguageHandler.new("unknown.strings"))
  47.     @strings = strings
  48.  
  49.     # PC Load paths will have a ':' after the drive letter.
  50.     @is_mac = ($LOAD_PATH[0][1..1] != ":")
  51.  
  52.     # Cache the online state. We will only recheck if we're offline.
  53.     @is_online = false
  54.  
  55.     # Find out if they've agreed to our EULA.
  56.     @agreed_to_eula = Sketchup.read_default WT_REGISTRY_SECTION,
  57.       WT_REGISTRY_KEY, false
  58.  
  59.     # Remember the last lat/lng we passed up from the shadow_info.
  60.     # If it changes, we'll tell the webdialog to reset.
  61.     @last_shadow_info_json = false
  62.  
  63.     # Create our dialog.
  64.     keys = {
  65.       :dialog_title => title,
  66.       :scrollable => false,
  67.       :preferences_key => WT_DIALOG_REGISTRY_KEY,
  68.       :height => WT_DIALOG_HEIGHT,
  69.       :width => WT_DIALOG_WIDTH,
  70.       :min_height => WT_DIALOG_MIN_HEIGHT,
  71.       :min_width => WT_DIALOG_MIN_WIDTH,
  72.       :left => WT_DIALOG_X,
  73.       :top => WT_DIALOG_Y,
  74.       :resizable => true,
  75.       :mac_only_use_nswindow => true}
  76.     @dialog = UI::WebDialog.new(keys)
  77.  
  78.     @dialog.set_background_color('000000')
  79.     @dialog.set_html('<body bgcolor="#FF0000"></body>')
  80.  
  81.     # Attach all of our callbacks.
  82.     @dialog.add_action_callback("grab") { |d, p| grab(p) }
  83.     @dialog.add_action_callback("agree_to_eula") { |d, p| agree_to_eula(p) }
  84.     @dialog.add_action_callback("store_ui_state") { |d, p| store_ui_state(p) }
  85.     @dialog.add_action_callback("pull_ui_state") { |d, p| pull_ui_state(p) }
  86.     @dialog.add_action_callback("get_flash") { |d, p| get_flash(p) }
  87.     @dialog.add_action_callback("pull_selected_shape") { |d,p|
  88.       pull_selected_shape(p)
  89.     }
  90.     @dialog.add_action_callback("open_url") { |d, p| open_url(p) }
  91.     @dialog.add_action_callback("open_eula") { |d, p| open_eula() }
  92.  
  93.     # A hash to store arbitrary state about the WebDialog's embedded html UI.
  94.     @ui_state = {}
  95.  
  96.     # Version string that will be written as an attribute onto any created
  97.     # material.
  98.     @version_ruby = "1.0.0"
  99.  
  100.     # Place where we will save the texture to.
  101.     if @is_mac
  102.       @image_path = temp_directory + '/temp.jpg'
  103.     else
  104.       @image_path = temp_directory + '\\temp.jpg'
  105.     end
  106.  
  107.     # Where to load the url from.
  108.     # TODO(scottlininger): Replace the default URL with the live one
  109.     # once it's live.
  110.     @url = Sketchup.get_datfile_info 'WEB_TEXTURES',
  111.         'http://sketchup.google.com/3dwarehouse/skptextures'
  112.     @dialog.set_url(@url)
  113.     show()
  114.   end
  115.  
  116.  
  117.   # A callback that opens a url in the default browser.
  118.   #
  119.   #   Args:
  120.   #     url: The URL
  121.   #
  122.   #   Returns:
  123.   #     Nothing
  124.   #
  125.   def open_url(url)
  126.     UI.openURL(url)
  127.   end
  128.  
  129.  
  130.   # A callback that opens the SketchUp EULA in the default browser.
  131.   #
  132.   #   Args:
  133.   #     None
  134.   #
  135.   #   Returns:
  136.   #     Nothing
  137.   #
  138.   def open_eula()
  139.     if Sketchup.is_pro?
  140.       url = Sketchup.get_datfile_info 'EULA',
  141.         'http://sketchup.google.com/intl/en/download/license.html'
  142.     else
  143.       url = Sketchup.get_datfile_info 'EULA_PRO',
  144.         'http://sketchup.google.com/intl/en/download/license_pro.html'
  145.     end
  146.     UI.openURL(url)
  147.   end
  148.  
  149.  
  150.   # A callback that allows javascript to get a string describing the shape
  151.   # of the currently selected face. If anything but a single face is selected,
  152.   # then a simple rectangle will be sent. By default, it replies to the
  153.   # javascript by setting a global js variable called 'shapeString', but
  154.   # if an optional param called oncomplete is passed, then it will call
  155.   # that function instead.
  156.   #
  157.   #   Args:
  158.   #     params: The params string that was sent as part of the callback. It
  159.   #             may have an optional "oncomplete" param.
  160.   #
  161.   #   Returns:
  162.   #     Nothing
  163.   #
  164.   def pull_selected_shape(params)
  165.     params = query_to_hash(params)
  166.  
  167.     # Figure out how many faces are selected.
  168.     selection = Sketchup.active_model.selection
  169.     faces = []
  170.     for entity in selection
  171.       faces.push entity if entity.typename == "Face"
  172.     end
  173.  
  174.     # Generate a string describing the shape if only one is selected.
  175.     # Otherwise just describe a rectangle shape.
  176.     uv_strings = []
  177.     if faces.length == 1
  178.       corners, vertex_uvs = get_uvs(faces[0])
  179.       for uv in vertex_uvs
  180.         uv_strings.push(uv['u'].to_s + ',' + uv['v'].to_s)
  181.       end
  182.     else
  183.       uv_strings.push('0,0')
  184.       uv_strings.push('1,0')
  185.       uv_strings.push('1,1')
  186.       uv_strings.push('0,1')
  187.     end
  188.     shape_string = uv_strings.join(':');
  189.  
  190.     # Figure out the width and height if only one face is selected.
  191.     # Otherwise just describe a square.
  192.     if corners.to_s != ""
  193.       width = corners[0].distance corners[1]
  194.       height = corners[1].distance corners[2]
  195.     else
  196.       width = 1
  197.       height = 1
  198.     end
  199.  
  200.     # Walk the selection and build a string describing the geometry. This
  201.     # will be encoded as a nested JSON array of x, y, z locations, like this:
  202.     #  
  203.     # [ 
  204.     #   [{x:0, y:0, z:0}, {x:1, y:1, z:0}, {x:1, y:0, z:1}],   // face 1
  205.     #   [{x:0, y:0, z:2}, {x:1, y:1, z:2}, {x:1, y:0, z:3}],   // face 2
  206.     #   [{x:0, y:0, z:4}, {x:1, y:1, z:4}, {x:1, y:0, z:5}]   // etc...
  207.     # ]
  208.     bb = Geom::BoundingBox.new()
  209.     geometry_json = '['
  210.     if faces.length <= WT_MAX_FACES_TO_PROCESS
  211.       loop_strings = []
  212.       for face in faces
  213.         vertex_strings = []
  214.         for vertex in face.outer_loop.vertices
  215.           bb.add(vertex.position)
  216.           loc = vertex.position
  217.           vertex_strings.push('{x:' + clean_for_json(loc.x.to_f) + ',' +
  218.               'y:' + clean_for_json(loc.y.to_f) + ',' +
  219.               'z:' + clean_for_json(loc.z.to_f) + '}')
  220.         end
  221.         loop_strings.push(vertex_strings.join(','))
  222.       end
  223.       geometry_json += loop_strings.join(',');
  224.     end
  225.     geometry_json += ']'
  226.     
  227.     # Calculate the latlng center of our selection.
  228.     latlng = Sketchup.active_model.point_to_latlong(bb.center)
  229.  
  230.     # Execute a JS command to reply.
  231.     if params['oncomplete'] != nil
  232.       cmd = params['oncomplete'] + '("' + shape_string +
  233.         '", ' + width.to_f.to_s + ', ' + height.to_f.to_s + ', "", ' +
  234.         clean_for_json(latlng[1].to_f) + ', ' +
  235.         clean_for_json(latlng[0].to_f) + ', ' +
  236.         geometry_json + ')'
  237.     else
  238.       cmd = "shapeString = '" + shape_string + "'"
  239.     end
  240.     @dialog.execute_script(cmd)
  241.  
  242.   end
  243.  
  244.  
  245.   # A callback that tells SketchUp that the user has agreed to our EULA. This
  246.   # will set a registry value to record that fact as a unix timestamp.
  247.   #
  248.   #   Args:
  249.   #     params: The params string that was sent as part of the callback. It
  250.   #             may have an optional "oncomplete" param.
  251.   #
  252.   #   Returns:
  253.   #     Nothing
  254.   #
  255.   def agree_to_eula(params)
  256.     params = query_to_hash(params)
  257.     Sketchup.write_default WT_REGISTRY_SECTION, WT_REGISTRY_KEY, Time.now.to_i
  258.     @agreed_to_eula = true
  259.   end
  260.  
  261.  
  262.   # A callback that allows the web dialog's Javascript to send down an
  263.   # arbitrary JSON state string that will be sent back up to the dialog
  264.   # should it be closed and reopened.
  265.   #
  266.   # Each JSON string is stored by key in the @ui_state hash. This allows for
  267.   # different sections of the WebDialog UI to store different states
  268.   # without clobbering each other. For example, Street View might want
  269.   # to store the current yaw, pan, and zoom, while a Picasa photo
  270.   # picker might want to store the current photo URL being viewed.
  271.   #
  272.   #   Args:
  273.   #     params: The params string that was sent as part of the callback. It is
  274.   #             expected to contain a param called "key" and "state" that has
  275.   #             the JSON string we care to store.
  276.   #
  277.   #   Returns:
  278.   #     Nothing
  279.   #
  280.   def store_ui_state(params)
  281.     params = query_to_hash(params)
  282.     @ui_state[params['key']] = params['state']
  283.   end
  284.  
  285.   # A callback that allows the web dialog's Javascript to request the
  286.   # JSON state that was sent down to Ruby via store_state.
  287.   #
  288.   #   Args:
  289.   #     params: The params string that was sent as part of the callback.
  290.   #
  291.   #   Returns:
  292.   #     Nothing
  293.   #
  294.   def pull_ui_state(params)
  295.     params = query_to_hash(params)
  296.  
  297.     json = generate_ui_state_json()
  298.  
  299.     # Execute a JS command to reply.
  300.     if params['oncomplete'] != nil
  301.       cmd = params['oncomplete'] + '(' + json + ')'
  302.     else
  303.       cmd = "uiState = " + json
  304.     end
  305.     @dialog.execute_script(cmd)
  306.   end
  307.  
  308.  
  309.   # A callback that tells the user they need to install flash. This has
  310.   # different behavior mac vs. pc. On mac, we show them some messages
  311.   # and send them to Adobe to run the install. On PC, we tell them what's
  312.   # happening and to expect an ActiveX install box.
  313.   #
  314.   #   Args:
  315.   #     params: The params string that was sent as part of the callback.
  316.   #             It could contain 'message' or 'message2', which defines what
  317.   #             messages to show the user, and 'url' which defines where to open
  318.   #             the user's browser on Mac to should they click the 'yes' option.
  319.   #             If none of these is passed down, we use default values.
  320.   #
  321.   #   Returns:
  322.   #     Nothing
  323.   #
  324.   def get_flash(params)
  325.     params = query_to_hash(params)
  326.  
  327.    if @is_mac
  328.  
  329.       # Show a Yes/No message box.
  330.       if params['message'] != nil
  331.         msg = params['message']
  332.       else
  333.         msg = @strings.GetString("Photo Textures requires the latest" +
  334.           " version of the Flash player. Would you like to install it now?")
  335.       end
  336.       response = UI.messagebox(msg, MB_YESNO);
  337.  
  338.       # If they said yes, open the install URL in their default browser.
  339.       if response == 6 # YES
  340.         if params['message2'] != nil
  341.           msg = params['message2']
  342.         else
  343.           msg = @strings.GetString("We will now send you to an installation" +
  344.             " page for Flash player. Once you are done with the install," +
  345.             " please restart SketchUp.")
  346.         end
  347.         response = UI.messagebox(msg, MB_OKCANCEL);
  348.  
  349.         if response == 1 # OK
  350.           if params['url'] != nil
  351.             url = params['url']
  352.           else
  353.             url = Sketchup.get_datfile_info 'INSTALL_FLASH',
  354.               'http://get.adobe.com/flashplayer/'
  355.           end
  356.           UI.openURL url
  357.         end
  358.       end
  359.       @dialog.close()
  360.     else
  361.       if params['message'] != nil
  362.         msg = params['message']
  363.       else
  364.         msg = @strings.GetString("Photo Textures requires the latest" +
  365.           " version of the Flash player. An installation box for this should" +
  366.           " appear shortly. (If it does not, please visit www.flash.com to" +
  367.           " install.) Once you have agreed to the installation, you may need" +
  368.           " to restart SketchUp.")
  369.       end
  370.       UI.messagebox(msg)
  371.     end
  372.   end
  373.  
  374.   # Generates a JSON string representing the current shadow_info
  375.   #
  376.   #   Args:
  377.   #     None
  378.   #
  379.   #   Returns:
  380.   #     json: string representing our current shadow_info.
  381.   #
  382.   def generate_shadow_info_json()
  383.     shadow_info = Sketchup.active_model.shadow_info
  384.     return '"shadow_info":{ ' +
  385.       '"city": "' + shadow_info["City"] + '", ' +
  386.       '"country":"' + shadow_info["Country"] + '", ' +
  387.       '"lat": "' + shadow_info["Latitude"].to_s + '", ' +
  388.       '"lng": "' + shadow_info["Longitude"].to_s + '" '
  389.   end
  390.  
  391.  
  392.   # Generates a JSON string representing our complete UI state.
  393.   #
  394.   # Since many of our web textures UI ideas involve some notion of geolocation,
  395.   # this callback will always report on the that info inside a key called
  396.   # 'shadow_info'. By default, it replies to the JavaScript by setting a global
  397.   # js variable called 'uiState', but if an optional param called oncomplete
  398.   # is passed, then it will call that function instead.
  399.   #
  400.   #   Args:
  401.   #     None
  402.   #
  403.   #   Returns:
  404.   #     json: string representing our current ui state.
  405.   #
  406.   def generate_ui_state_json()
  407.  
  408.     # Build out a JSON string of all of our state info.
  409.     json = '{'
  410.         
  411.     if @agreed_to_eula
  412.       json += '"agreedToEula":"' + @agreed_to_eula.to_s + '",';
  413.     end
  414.  
  415.     # Figure out the lat/lng of the center point of the current selection.
  416.     # This will be passed up to the WebDialog so we can use it to improve
  417.     # pose guessing.
  418.     selection = Sketchup.active_model.selection
  419.     if selection.length > 0 && selection.length <= WT_MAX_FACES_TO_PROCESS
  420.       bb = Geom::BoundingBox.new()
  421.       faces = []
  422.       for entity in selection
  423.         if entity.typename == "Face"
  424.           faces.push entity
  425.           for vertex in entity.vertices
  426.             bb.add(vertex.position)
  427.           end
  428.         end
  429.       end
  430.       latlng = Sketchup.active_model.point_to_latlong(bb.center)
  431.       json += '"selectionLat":"' + clean_for_json(latlng[1].to_f) + '",';
  432.       json += '"selectionLng":"' + clean_for_json(latlng[0].to_f) + '",';
  433.       json += '"selectionAlt":"' + clean_for_json(latlng[2].to_f) + '",';
  434.  
  435.       # If a single face is selected, calculate a lat/lng that is 50'
  436.       # in front of the face. This gives much better Street View
  437.       # pose guesses for looking at faces that are along the "sides"
  438.       # of buildings.
  439.       if faces.length == 1
  440.         vector = faces.first.normal
  441.         vector.length = 12.0 * 50.0
  442.         offset_pt = bb.center.offset vector
  443.         latlng = Sketchup.active_model.point_to_latlong(offset_pt)
  444.         json += '"selectionOffsetLat":"' + clean_for_json(latlng[1].to_f) + '",';
  445.         json += '"selectionOffsetLng":"' + clean_for_json(latlng[0].to_f) + '",';
  446.         json += '"selectionOffsetAlt":"' + clean_for_json(latlng[2].to_f) + '",';
  447.       end
  448.     end
  449.  
  450.  
  451.  
  452.     for key in @ui_state.keys
  453.       json += '"' + key + '":' + @ui_state[key] + ','
  454.     end
  455.     shadow_info_json = generate_shadow_info_json()
  456.     
  457.     # Store the last lat/lng so we know if the user resets them we can
  458.     # reset the WebDialog's view.
  459.     if shadow_info_json != @last_shadow_info_json
  460.       json += shadow_info_json + ', "hasChanged": 1}}'
  461.     else
  462.       json += shadow_info_json + '}}'
  463.     end
  464.  
  465.     @last_shadow_info_json = shadow_info_json
  466.     return json
  467.   end
  468.  
  469.   # A callback that allows the web dialog to tell SketchUp to take a screen
  470.   # grab of the current dialog and apply that texture to selected faces.
  471.   #
  472.   # It expects a param called region that defines four x,y corners of the
  473.   # pixel region to map to the currently selected face(s). Each of these
  474.   # interior corners will be UV mapped onto the 4 geometric corners of the
  475.   # Face. (In the case of a non-rectangular face, it will calculate "virtual"
  476.   # corners that bound the face and map to those instead.)
  477.   #
  478.   # The string will be four x,y local texture coordinates separated by colons.
  479.   # A typical string might look like this... '10,90:120,100:120,5:10,5'
  480.   #
  481.   # The first corner in the list (c0) is the bottom left, and they go
  482.   # counter-clockwise from there. The x,y origin (0,0) is located at the top
  483.   # left of the texture image.
  484.   #
  485.   #   c3----------c2
  486.   #   |           |
  487.   #   c0----      |
  488.   #         ------c1
  489.   #
  490.   #   Args:
  491.   #     params: The params string that was sent as part of the callback.
  492.   #
  493.   #   Returns:
  494.   #     Nothing, but it does call a Javascript method called onGrabComplete()
  495.   #     when it is complete.
  496.   #
  497.   def grab(params)
  498.     begin
  499.       params = query_to_hash(params)
  500.  
  501.       # Make a list of the faces to texture.
  502.       faces_to_texture = []
  503.       selection = Sketchup.active_model.selection
  504.       for face in selection
  505.         if face.typename == "Face"
  506.           faces_to_texture.push(face)
  507.         end
  508.       end
  509.  
  510.       # Bail out if there are no selected faces.
  511.       if faces_to_texture.length == 0
  512.         UI.messagebox(@strings.GetString("Please select one or more faces" +
  513.           " in your SketchUp model that you would like to photo texture" +
  514.           " and try again."))
  515.         if params['oncomplete'] != nil
  516.           @dialog.execute_script(params['oncomplete'])
  517.         end
  518.         return
  519.       end
  520.  
  521.       op = @strings.GetString("Apply Photo Texture")
  522.       Sketchup.active_model.start_operation op, true
  523.  
  524.       # Capture the screen and create the material.
  525.       if params['compression'] == nil
  526.         params['compression'] = 75
  527.       end
  528.       if params['top_left_x'] == nil
  529.         @dialog.write_image(@image_path, params['compression'].to_i)
  530.       else
  531.         @dialog.write_image(@image_path, params['compression'].to_i,
  532.           params['top_left_x'].to_i,
  533.           params['top_left_y'].to_i,
  534.           params['bottom_right_x'].to_i,
  535.           params['bottom_right_y'].to_i)
  536.       end
  537.      
  538.  
  539.       file = @image_path.gsub(/\\/, '/')
  540.       materials = Sketchup.active_model.materials
  541.       m = materials.add @strings.GetString("Photo Texture")
  542.       m.texture = file
  543.       texture = m.texture
  544.       
  545.       if params['texture_width'] == nil
  546.         texture.size = WT_DEFAULT_TEXTURE_WIDTH
  547.       else
  548.         texture.size = params['texture_width'].to_f
  549.       end
  550.       
  551.       pixel_width = texture.image_width.to_f
  552.       pixel_height = texture.image_height.to_f
  553.  
  554.       # Attach some attributes to the material so we can view on 3D Warehouse.
  555.       m.set_attribute "web_textures", "version_ruby", @version_ruby
  556.       m.set_attribute "web_textures", "ui_state", generate_ui_state_json()
  557.       m.set_attribute "web_textures", "created", Time.now.to_i
  558.  
  559.       # If a region param was passed that defines some UV mapping info, then
  560.       # do UV mapping. Otherwise, just paint all faces with the untransformed
  561.       # texture.
  562.       if params['region'] != nil
  563.         uvs = []
  564.         corners = params['region'].split(':')
  565.         for corner in corners
  566.           u, v = corner.split(',')
  567.           u = u.to_f
  568.           v = v.to_f
  569.           u = u / pixel_width
  570.           v = (pixel_height - v) / pixel_height
  571.           uvs.push(u.to_s + ',' + v.to_s)
  572.         end
  573.  
  574.         if faces_to_texture.length == 1
  575.           # Apply the texture to the side of the face the camera is looking at.
  576.           # TODO(scottlininger): Could be better to rewrite to use the plane
  577.           # equation. See http://mondrian.corp.google.com/file/11865235 for
  578.           # commentary.
  579.           face = faces_to_texture[0]
  580.           camera_direction = Sketchup.active_model.active_view.camera.direction
  581.           angle = face.normal.angle_between camera_direction
  582.           if angle.radians < 90
  583.             uv_texture(face, m, uvs, false, true)
  584.           else
  585.             uv_texture(face, m, uvs, true, false)
  586.           end
  587.         else
  588.           # Apply the texture to the front of all selected faces.
  589.           for face in faces_to_texture
  590.             uv_texture(face, m, uvs, true, false)
  591.           end
  592.         end
  593.         
  594.       else
  595.         # Paint the texture onto all selected faces.
  596.         for face in faces_to_texture
  597.           face.material = m
  598.         end
  599.       end
  600.  
  601.       if params['oncomplete'] != nil
  602.         @dialog.execute_script(params['oncomplete'])
  603.       end
  604.  
  605.       # Delete the temporary jpg file.
  606.       File.delete(file)
  607.  
  608.       Sketchup.active_model.commit_operation
  609.  
  610.     rescue Exception => e
  611.       puts "#{e.class}: #{e.message}"
  612.       UI.messagebox(
  613.           @strings.GetString("There was an error pulling in the texture.") +
  614.           "\n" +
  615.           @strings.GetString("Please try again.") + "\n\n" +
  616.           "#{e.class}: #{e.message}")
  617.       if params['oncomplete'] != nil
  618.         @dialog.execute_script(params['oncomplete'])
  619.       end
  620.     end
  621.   end
  622.  
  623.  
  624.   # UV Textures a face, meaning it applies a texture and positions it to match
  625.   # four coordinate pairings passed in. Each "corner" of the face will
  626.   # get a corresponding u,v coordinate local to the texture itself, and
  627.   # SketchUp will scale and skew the texture so that the u,v location matches
  628.   # with each corner.
  629.   #
  630.   #   Args:
  631.   #     face:     The face to texture.
  632.   #     material: The Material object to apply. It must already contain the
  633.   #               texture.
  634.   #     uvs:      An 4-element array of strings. Each string is a single u,v
  635.   #               coordinate such as "0,0" or ".25,1.0"
  636.   #     do_front: If true, apply to front of face.
  637.   #     do_back:  If true, apply to back of face.
  638.   #
  639.   #   Returns:
  640.   #     Nothing
  641.   def uv_texture(face, material, uvs, do_front=true, do_back=false)
  642.     corners, vertex_uvs = get_uvs(face)
  643.     pts = []
  644.     for i in 0..3
  645.       pts << corners[i].to_a
  646.       uv = uvs[i].split(',')
  647.       pts << [uv[0].to_f,uv[1].to_f]
  648.     end
  649.     if do_front
  650.       face.position_material material, pts, true
  651.     end
  652.     if do_back
  653.       back_pts = []
  654.       back_pts[0] = pts[2]
  655.       back_pts[1] = pts[1]
  656.       back_pts[2] = pts[0]
  657.       back_pts[3] = pts[3]
  658.       back_pts[4] = pts[6]
  659.       back_pts[5] = pts[5]
  660.       back_pts[6] = pts[4]
  661.       back_pts[7] = pts[7]
  662.       face.position_material material, back_pts, false
  663.     end
  664.   end
  665.  
  666.  
  667.   # Turns a query string into a hash. So something like "x=100&z=2" will be
  668.   # translated into { x:"100", z:"2" }.
  669.   #
  670.   #   Args:
  671.   #     data: The string to process.
  672.   #
  673.   #   Returns:
  674.   #     param_hash: The nice name/value paired hash.
  675.   def query_to_hash(data)
  676.     param_pairs = data.to_s.split('&')
  677.     param_hash = {}
  678.     for param in param_pairs
  679.       name, value = param.split('=')
  680.       param_hash[name] = value
  681.     end
  682.     return param_hash
  683.   end
  684.  
  685.  
  686.   # Pops open the dialog.
  687.   #
  688.   #   Args:
  689.   #     None.
  690.   #
  691.   #   Returns:
  692.   #     Nothing
  693.   def show(force_refresh = false)
  694.  
  695.     # If we don't think we're connected, or if it's the first time we've
  696.     # launched the dialog, then ask SketchUp if we're online.
  697.     if @is_online == false
  698.       @is_online = Sketchup.is_online
  699.     end
  700.     
  701.     # If we're still offline, show a message.
  702.     if @is_online == false
  703.       UI.messagebox(@strings.GetString("Photo Textures requires a connection " +
  704.         "to the internet and yours appears to be down. Please reset " +
  705.         "your connection and try again."))
  706.       return
  707.     end
  708.  
  709.     # If the geo location has changed, force a refresh of the dialog.
  710.     if @dialog.visible?
  711.       if force_refresh == true
  712.         @dialog.execute_script('refresh()');
  713.       end
  714.     end
  715.  
  716.     if @dialog.visible? == false
  717.       if @is_mac
  718.         # Mac has refresh issues with flash, so reset URL.
  719.         @dialog.set_url(@url)
  720.         @dialog.show_modal
  721.       else
  722.         @dialog.show
  723.       end
  724.     end
  725.  
  726.     if @is_mac
  727.       # Force focus on the mac.
  728.       @dialog.bring_to_front
  729.     end
  730.  
  731.   end
  732.  
  733.  
  734.   # There are two things that we calculate in this function: first, the
  735.   # 4 "corners" in model space that define a rectangular bounding poly of the
  736.   # underlying face. Second, an array of the uv points for each vertex in the
  737.   # face, relative to that bounding poly.
  738.   #
  739.   #   Args:
  740.   #     face: the face to calculate corners and vertex uvs for
  741.   #
  742.   #   Returns:
  743.   #     corners:    An Array of Point3d objects describing the four "corners"
  744.   #                 surrounding the face. These may or may not be vertices. For
  745.   #                 example, a circular face or a diamond will have 4 corners
  746.   #                 where none of them match up with a vertex. A square face
  747.   #                 will have all 4 corners overlap with a vertex.
  748.   #     vertex_uvs: An Array of hashes. Each hash contains a "u" and a "v"
  749.   #                 member, so that you'll get something like this:
  750.   #                 [ {u:0,v:1}, {u:0.75,v:.25}, {u:0.25,v:.75}, {u:0.25,v:.75}]
  751.   #
  752.   def get_uvs(face)
  753.  
  754.     # Get the axes for a plane that the face is on, with
  755.     # the x axis parallel to the ground plane and the z axis
  756.     # corresponding to the face normal.
  757.     xaxis, yaxis, zaxis = face.normal.axes
  758.  
  759.     # Calculate points that define a "bottom" and a "left"
  760.     # direction, as would be viewed by a person looking from
  761.     # the camera toward the face with their feet pointing
  762.     # downward. (In the case of a face that is parallel to
  763.     # the ground, this method will return "bottom" as being
  764.     # in the negative y direction.)
  765.     far_left = xaxis.reverse
  766.     far_left.length = WT_VERY_LARGE_NUMBER
  767.     left = face.vertices[0].position
  768.     left.offset! far_left
  769.  
  770.     far_bottom = yaxis.reverse
  771.     far_bottom.length = WT_VERY_LARGE_NUMBER
  772.     bottom = face.vertices[0].position
  773.     bottom.offset! far_bottom
  774.  
  775.     # Figure out which face vertices define the 4 edges of
  776.     # our bounding poly. Assume it's the first one for the moment.
  777.     left_most_pt = face.vertices[0].position
  778.     right_most_pt = face.vertices[0].position
  779.     top_most_pt = face.vertices[0].position
  780.     bottom_most_pt = face.vertices[0].position
  781.  
  782.     # Look at each vertex and decide if it's a better fit for
  783.     # being a bounding point.
  784.     for i in 1..(face.vertices.length-1)
  785.       pt = face.vertices[i].position
  786.  
  787.       if pt.distance(left) < left_most_pt.distance(left)
  788.         left_most_pt = pt
  789.       elsif pt.distance(left) > right_most_pt.distance(left)
  790.         right_most_pt = pt
  791.       end
  792.  
  793.       if pt.distance(bottom) < bottom_most_pt.distance(bottom)
  794.         bottom_most_pt = pt
  795.       elsif pt.distance(bottom) > top_most_pt.distance(bottom)
  796.         top_most_pt = pt
  797.       end
  798.     end
  799.  
  800.     # Now that we have all four bounding edges, calculate
  801.     # the 4 corners of our bounding poly.
  802.     left_line = [left_most_pt, yaxis]
  803.     right_line = [right_most_pt, yaxis]
  804.     top_line = [top_most_pt, xaxis]
  805.     bottom_line = [bottom_most_pt, xaxis]
  806.  
  807.     corners = []
  808.     corners << Geom.intersect_line_line(left_line, bottom_line)
  809.     corners << Geom.intersect_line_line(right_line, bottom_line)
  810.     corners << Geom.intersect_line_line(right_line, top_line)
  811.     corners << Geom.intersect_line_line(left_line, top_line)
  812.  
  813.     # Now that we've calculated a perfect "bounding rectangle" for the
  814.     # face, we can calculate u,v coordinates within that little
  815.     # coordinate space. The "bottom left" corner of the rectangle is
  816.     # at uv point 0,0 and the "top right" is at 1,1. Therefore, all of
  817.     # the vertex u,v coordinates will lie between 0 and 1.
  818.     uvs = []
  819.     w = right_most_pt.distance_to_line(left_line)
  820.     h = top_most_pt.distance_to_line(bottom_line)
  821.     for vertex in face.outer_loop.vertices
  822.       uv = {}
  823.       uv['u'] = vertex.position.distance_to_line(left_line) / w
  824.       uv['v'] = vertex.position.distance_to_line(bottom_line) / h
  825.       uvs.push uv
  826.     end
  827.  
  828.     return corners, uvs
  829.   end
  830.  
  831.  
  832.   # Cleans up strings to inclusion inside JSON string values
  833.   #
  834.   #   Args:
  835.   #      value: a string that we want escaped
  836.   #
  837.   #   Returns:
  838.   #      string: a JSON-friendly version suitable for parsing in javascript
  839.   def clean_for_json(value)
  840.     value = value.to_s
  841.     value = value.gsub(/\\/,'\')
  842.     value = value.gsub(/\"/,'"')
  843.     value = value.gsub(/\n/,'\n')
  844.     if value.index(/e-\d\d\d/) == value.length-5
  845.       value = "0.0";
  846.     end
  847.     return value
  848.   end
  849.  
  850.  
  851.   # Returns the system's temporary directory.
  852.   #
  853.   #   Args:
  854.   #      none
  855.   #
  856.   #   Returns:
  857.   #      string: the pull path to the temp directory.
  858.   def temp_directory
  859.     if @temp_dir
  860.       return @temp_dir
  861.     end
  862.     tmp = '.'
  863.     for dir in [ENV['TMPDIR'], ENV['TMP'], ENV['TEMP'],
  864.         ENV['USERPROFILE'], '/tmp']
  865.       if dir and File.directory?(dir) and File.writable?(dir)
  866.         tmp = dir
  867.         break
  868.       end
  869.     end
  870.     @temp_dir = File.expand_path(tmp)
  871.     return @temp_dir
  872.   end
  873. end
  874.  
  875.  
  876. #
  877. # Set up the UI hooks for the standard grab texture functionality.
  878. #
  879. #
  880. #
  881. #
  882. #
  883. if (not $wt_loaded)
  884.  
  885.   # Create the context menu item.
  886.   UI.add_context_menu_handler do |context_menu|
  887.     selection = Sketchup.active_model.selection
  888.     has_faces = false
  889.     for entity in selection
  890.       if entity.typename == "Face"
  891.         has_faces = true
  892.         break
  893.       end
  894.     end
  895.     if has_faces
  896.       context_menu.add_separator
  897.       context_menu.add_item($wt_strings.GetString("Add Photo Texture")) {
  898.         if not $wt_instance
  899.           $wt_instance = WebTextures.new($wt_strings.GetString("Photo Textures"),
  900.             $wt_strings)
  901.         else
  902.           $wt_instance.show(true)
  903.         end
  904.       }
  905.     end
  906.   end
  907.  
  908.   # Create the Windows > Web Textures menu item.
  909.   menu = UI.menu("Windows")
  910.   menu_text = $wt_strings.GetString("Photo Textures")
  911.   cmd = UI::Command.new(menu_text) {
  912.     if not $wt_instance
  913.       $wt_instance = WebTextures.new($wt_strings.GetString("Photo Textures"),
  914.         $wt_strings)
  915.     else
  916.       $wt_instance.show()
  917.     end
  918.   }
  919.   cmd.tooltip = menu_text
  920.   menu.add_item(cmd)
  921.   $wt_loaded = true
  922. end
  923.