Learning to Modularize

I am learning to write code for SketchUp and over a series of days to successfully script a roller coaster importer from a program called No Limits 2. My code does a bit too much to understand and keep straight at the moment and I want to begin to modularize the whole thing in to multiple scripts. Right now the code parses data, draws rails, and creates components placed along a spline path, all while maintaining the banking information attached to the imported spline.

I have been working to modularize my code but can’t seem to get anything to work once the scripts are separated. I have tried using Visual Studio Code and the Ruby Code Editor extension. I haven’t found any resources teaching how to properly set up a directory for extension development, where that directory needs to be, if it needs to be signed, or how to get a script to call modules/tools in other scripts that are in the same directory.

Right now I would like to break my current code in to a main.rb, ui.rb, data_parser.rb, tuber_generator.rb, tie_generate_placer.rb. My future plans include an XML importer for importing structural support data, a square track spine generator, a clean up function, a tagging module, a wireframe module, and more.

Try my code out! It’s exciting to see a roller coaster in SketchUp! Please let me know what insights you have, advice you have for resources, or if anybody feels interested in one-one-one tutoring. I am happy to pay, though I can’t afford much, just really excited to be learning!

require 'csv'
require 'sketchup'

def import_and_tube_track
  model = Sketchup.active_model
  definitions = model.definitions
  file_path = UI.openpanel("Select CSV File", "", "CSV Files|*.csv;||")
  return unless file_path

  prompts = ["Distance between rails (m)", "Spine vertical offset (m)", "Diameter of rails (m)", "Diameter of spine (m)", "Amount of segments per tube", "Interval for drawing (every n points)"]
  defaults = ["1.2", "-0.4", "0.158", "0.513", "8", "2"]
  input = UI.inputbox(prompts, defaults, "Enter Tube Specifications and Drawing Interval")
  return unless input

  total_rail_distance, spine_vertical_offset, rail_diameter, spine_diameter, segments, every_n_points = input.map(&:to_f)
  rail_offset = (total_rail_distance / 2) * 39.3701
  spine_vertical_offset = spine_vertical_offset * 39.3701
  rail_diameter = rail_diameter * 39.3701  # Convert meters to inches
  spine_diameter = spine_diameter * 39.3701  # Convert meters to inches

  spine_points = []
  left_rail_points = []
  right_rail_points = []

  csv_options = { headers: true, col_sep: "\t", quote_char: '"', skip_blanks: true, liberal_parsing: true }
  
  CSV.foreach(file_path, **csv_options).with_index do |row, i|
    next if i % every_n_points != 0
    x, y, z = row['PosX'].to_f * 39.3701, row['PosZ'].to_f * 39.3701, row['PosY'].to_f * 39.3701
    center_point = Geom::Point3d.new(x, y, z)

    up_vector = Geom::Vector3d.new(row['UpX'].to_f, row['UpZ'].to_f, row['UpY'].to_f)
    up_vector.length = spine_vertical_offset
    spine_points << center_point.offset(up_vector)

    left_vector = Geom::Vector3d.new(row['LeftX'].to_f, row['LeftZ'].to_f, row['LeftY'].to_f)
    left_vector.length = rail_offset
    right_vector = left_vector.reverse
    right_vector.length = rail_offset

    left_rail_points << center_point.offset(left_vector)
    right_rail_points << center_point.offset(right_vector)
  end

  model.start_operation('Create Rails, Spine, and Track Ties', true)
  rails_and_spine_group = model.active_entities.add_group
  create_tubes(rails_and_spine_group, spine_points, spine_diameter, segments)
  create_tubes(rails_and_spine_group, left_rail_points, rail_diameter, segments)
  create_tubes(rails_and_spine_group, right_rail_points, rail_diameter, segments)

  track_ties_group = create_and_place_track_ties(file_path, total_rail_distance, spine_vertical_offset)
  main_group = model.active_entities.add_group([rails_and_spine_group, track_ties_group])

  # Mirror transformation across the green axis at the origin
  mirror_transformation = Geom::Transformation.scaling(1, -1, 1)
  main_group.transform!(mirror_transformation)

  model.commit_operation
end

def create_tubes(group, points, diameter, segments)
  return if points.length < 2
  spline = group.entities.add_curve(points)
  direction_vector = points[1] - points[0]
  circle = group.entities.add_circle(points.first, direction_vector, diameter / 2, segments)
  face = group.entities.add_face(circle)
  face.followme(spline)
end

def create_and_place_track_ties(file_path, total_rail_distance, spine_vertical_offset)
  model = Sketchup.active_model
  definitions = model.definitions
  track_tie_def = definitions['Triangular Track Tie'] || definitions.add("Triangular Track Tie")
  entities = track_tie_def.entities
  entities.clear!

  half_rail_distance = (total_rail_distance / 2) * 39.3701
  spine_offset = spine_vertical_offset  # Use the converted spine_vertical_offset directly

  # Define the triangular track tie geometry
  pts = [
    [-half_rail_distance, 0, 0],  # Left rail end
    [half_rail_distance, 0, 0],   # Right rail end
    [0, spine_offset, 0]          # Center of the spine
  ]
  base_face = entities.add_face(pts)
  thickness = 8.cm  # Convert cm to inches within SketchUp API
  base_face.pushpull(-thickness / 2)
  base_face.pushpull(thickness / 2)

  track_ties_group = model.active_entities.add_group

  # Read CSV and place track ties
  csv_options = { headers: true, col_sep: "\t", quote_char: '"', skip_blanks: true, liberal_parsing: true }
  CSV.foreach(file_path, **csv_options) do |row|
    x = row['PosX'].to_f * 39.3701  # Convert meters to inches
    y = row['PosZ'].to_f * 39.3701  # Convert meters to inches
    z = row['PosY'].to_f * 39.3701  # Convert meters to inches
    position = Geom::Point3d.new(x, y, z)

    up_vector = Geom::Vector3d.new(row['UpX'].to_f, row['UpZ'].to_f, row['UpY'].to_f).normalize
    left_vector = Geom::Vector3d.new(row['LeftX'].to_f, row['LeftZ'].to_f, row['LeftY'].to_f).normalize
    transformation = Geom::Transformation.new(position, left_vector, up_vector)
    instance = track_ties_group.entities.add_instance(track_tie_def, transformation)
  end

  track_ties_group
end

# Trigger the function to start the process
import_and_tube_track

Kumba.skp (2.0 MB)
kumba.csv.zip (85.2 KB)

Did you mean to attach another sample file?

I need to upload a .csv file but it’s on my computer back at the office. I will have to edit this post tomorrow. Instead I uploaded the output of what the script does with the .csv

The csv would be good. By output of what the script does did you mean the SKP file (Kumba.skp)? If so, that needs to be excluded from the triple backticks for the link to work… and I see you just did that!

Just accessed my work computer remotely, csv added!

1 Like

This may be a good place for you to start:

[Template] Multi-File, Multi-Class with SharedConstants Mixin - Developers / Ruby API - SketchUp Community

See the Technical Requirements section of …

When the Extension Manager installs a RBZ archive, the extension registrar file and the extension subfolder will be extracted to the user’s %AppData% ("~/Library/Application Support" on Mac) path "Plugins" folder for the running version of SketchUp. Ie (at the console) …

Sketchup.find_support_file("Plugins")
=> "C:/Users/Dan/AppData/Roaming/SketchUp/SketchUp 2024/SketchUp/Plugins"

However, some users use other tools (like the SketchUcation PluginStore Tools or Fredo’s Alternate Plugins Path extension) to install to other alternate paths, possibly on another disk drive. IT admins in companies might even move an extension to the %ProgramData% path "Plugins" folder for all users of a workstation. (SketchUp loads extensions from %ProgramData% path before the user %AppData% path plugins.)

The takeaway is to never assume your extension is running from a particular path on the user’s machine. Use the global __dir__ method to get the path of the code file being evaluated when building paths to support files with File.join. Ex:

large_icon = File.join(__dir__, 'icons', 'nifty_tool.svg')

See also …

See …

NOTE: I just updated this today (2024-05-10) as it was in serious need of an overhaul.
(It still referenced the Extensions panel of the Preferences dialog, so it was pre-2017.)

2 Likes