class Oozby::Preprocessor

The Oozby Method Preprocessor handles requests via the transform_call method and transforms the Oozby::Element passed in, patching in any extra features and trying to alert the user of obvious bugs

Constants

DefaultOpenSCADMethods

list of OpenSCAD standard methods - these can pass through without error:

NoResolution

never pass resolution data in to these methods - it’s pointless:

ResolutionNames

apply resolution settings to element

Attributes

call[RW]
openscad_methods[RW]

Public Class Methods

default_filters(*list) click to toggle source

sets a list of default filters which aren’t reset after each method def

# File lib/oozby/preprocessor.rb, line 11
def default_filters *list
  @@default_filters = list.map { |x| if x.is_a? Array then x else [x] end }
  @@queued_filters = @@default_filters.dup
end
filter(filter_name, *options) click to toggle source

set a filter to be added only to the next method def

# File lib/oozby/preprocessor.rb, line 17
def filter filter_name, *options
  @@queued_filters ||= @@default_filters.dup
  @@queued_filters.delete_if { |x| x[0] == filter_name } # replace default definitions
  @@queued_filters.push([filter_name, *options]) # add new filter definition
end
filters_for(method_name) click to toggle source

get list of filters for a method name

# File lib/oozby/preprocessor.rb, line 41
def filters_for method_name
  @@method_filters[method_name] || []
end
finalize_filter(method_name) click to toggle source
# File lib/oozby/preprocessor.rb, line 29
def finalize_filter method_name
  @@method_filters[method_name] = @@queued_filters
  @@queued_filters = @@default_filters.dup
end
method_added(method_name) click to toggle source

detect a method def, store it’s filters and reset for next def

Calls superclass method
# File lib/oozby/preprocessor.rb, line 24
def method_added method_name
  finalize_filter method_name
  super
end
new(env: nil, ooz: nil) click to toggle source

setup a new method preprocessor

# File lib/oozby/preprocessor.rb, line 72
def initialize env: nil, ooz: nil
  @env = env
  @parent = ooz
  @openscad_methods = DefaultOpenSCADMethods.dup
end
oozby_alias(from, to, **extra_args) click to toggle source

alias an name to an openscad method optionally with extra defaults useful for giving things more descriptive names, where those names imply different defaults, like hexagon -> circle(sides: 6)

# File lib/oozby/preprocessor.rb, line 48
def oozby_alias from, to, **extra_args
  define_method(from) do
    call.named_args.merge!(extra_args) #{ |key,l,r| l } # left op wins conflicts
    run_filters to
    redirect to
  end
end
passthrough(method_name, *arg_names) click to toggle source

don’t want to define a primary processor method? pass it through manually

# File lib/oozby/preprocessor.rb, line 35
def passthrough method_name, *arg_names
  arg_list = arg_names.map { |x| "#{x}: nil" }.join(', ')
  define_method method_name, &eval("->(#{arg_list}) {nil}")
end

Public Instance Methods

args_parse(info, *arg_names) click to toggle source

parse arguments like openscad does

# File lib/oozby/preprocessor.rb, line 155
def args_parse(info, *arg_names)
  args = info.named_args.dup
  info.args.length.times do |index|
    warn "Overwriting argument #{arg_names[index]}" if args.key? arg_names[index]
    args[arg_names[index]] = info.args[index]
  end
  
  args
end
capture(&proc) click to toggle source

capture contents of a block as openscad code, returning AST array

# File lib/oozby/preprocessor.rb, line 166
def capture &proc
  env = @env
  (env._subscope {
    env.preprocessor(false) {
      env._execute_oozby(&proc)
    }
  }).find { |x|
    x.is_a? Oozby::Element
  }.tap { |x|
    x.modifier = "#{call.modifier}#{x.modifier}" if call.modifier
  }
end
cube(size: [1,1,1], center: false, corner_radius: 0) click to toggle source
# File lib/oozby/preprocessor-definitions.rb, line 25
def cube size: [1,1,1], center: false, corner_radius: 0
  return rounded_rectangular_prism(size: size, center: center, corner_radius: corner_radius) if corner_radius > 0
  return call
end
cylinder(h: 1, r1: nil, r2: nil, r: nil, center: false, corner_radius: 0) click to toggle source
# File lib/oozby/preprocessor-definitions.rb, line 34
def cylinder h: 1, r1: nil, r2: nil, r: nil, center: false, corner_radius: 0
  r1, r2 = r, r if r unless r1 || r2
  return rounded_cylinder(h: h, r1: r1, r2: r2, center: center, corner_radius: corner_radius) if corner_radius > 0
  return call
end
known() click to toggle source

array of all known method names

# File lib/oozby/preprocessor.rb, line 140
def known
  list = @openscad_methods.dup
  list.push *primary_processors
  list.push *@@method_filters.keys
  list.uniq
end
known?(name) click to toggle source

does this processor know of a method named whatever?

# File lib/oozby/preprocessor.rb, line 135
def known? name
  known.include? name.to_sym
end
primary_processors() click to toggle source

list of primary processor methods

# File lib/oozby/preprocessor.rb, line 130
def primary_processors
  @primary_processors ||= public_methods(false) - @@system_methods
end
redirect(new_method) click to toggle source

rewrite this method to a different method name and primary processor and whatever else

# File lib/oozby/preprocessor.rb, line 148
def redirect new_method
  call.method = new_method.to_sym
  public_send(new_method, *primary_method_args) if self.respond_to? new_method
end
run_filters(method_name) click to toggle source
# File lib/oozby/preprocessor.rb, line 104
def run_filters method_name
  # apply the other filters
  filters = self.class.filters_for(method_name)
  filters.each do |filter_data|
    filter_name, *filter_args = filter_data
    send(filter_name, *filter_args)
  end
end
square(size: [1,1], center: false, corner_radius: 0) click to toggle source
# File lib/oozby/preprocessor-definitions.rb, line 48
def square size: [1,1], center: false, corner_radius: 0
  return rounded_rectangle(size: size, center: center, corner_radius: corner_radius) if corner_radius > 0
  return call
end
transform_call(call_info) click to toggle source

accepts an Oozby::Element and transforms it according to the processors’ rules

# File lib/oozby/preprocessor.rb, line 79
def transform_call call_info
  raise "call info isn't Oozby::Element #{call_info.inspect}" unless call_info.is_a? Oozby::Element
  @call = call_info
  @original_method = @call.method
  
  run_filters call_info.method.to_sym
  
  methods = primary_processors
  # if a primary processor is defined for this kind of call
  if methods.include? call_info.method.to_sym
    # call the primary processor
    result = public_send(call_info.method, *primary_method_args)
    
    # replace the ast content with the processor's output
    if result.is_a? Hash or result.is_a? Oozby::Element
      # replace called item with this new stuff
      return result
    elsif result != nil # ignore nil - we don't need to do anything for that!
      raise "#{@original_method} preprocessor returned invalid result #{result.inspect}"
    end
  end
  
  return call_info
end

Private Instance Methods

expanded_names(height_label: :h) click to toggle source

general processing of arguments:

-o> Friendly names - use radius instead of r if you like
-o> Ranges - can specify radius: 5...10 instead of r1: 5, r2: 10
-o> Make h/height consistent (either works everywhere)
-o> Support inner radius, when number of sides is specified
-o> Specify diameter and have it halved automatically
# File lib/oozby/preprocessor-definitions.rb, line 167
def expanded_names height_label: :h
  # let users use 'radius' as longhand for 'r', and some other stuff
  rename_args(
    [:radius] => :r,
    [:radius1, :radius_1] => :r1,
    [:radius2, :radius_2] => :r2,
    [:facets, :fragments, :sides] => :"$fn",
    [:inr, :inradius, :in_radius, :inner_r, :inner_radius] => :ir,
    [:width] => :diameter,
    [:height, :h] => height_label
  )
  
  # let users specify diameter instead of radius - convert it
  {  diameter: :r,                    dia: :r,               d: :r,
    diameter1: :r1, diameter_1: :r1, dia1: :r1, dia_1: :r1, d1: :r1,
    diameter2: :r2, diameter_2: :r2, dia2: :r2, dia_2: :r2, d2: :r2,
    id: :ir, inner_diameter: :ir, inner_d: :ir,
    id1: :ir1, inner_diameter_1: :ir1, inner_diameter1: :ir1,
    id2: :ir2, inner_diameter_2: :ir2, inner_diameter2: :ir2
  }.each do |d, r|
    if call.named_args.key? d
      data = call.named_args.delete(d)
      if data.is_a? Range
        data = Range.new(data.first / 2.0, data.last / 2.0, data.exclude_end?)
      elsif data.respond_to? :to_f
        data = data.to_f / 2.0
      else
        raise "#{data.inspect} must be Numeric or a Range"
      end
      call.named_args[r] = data
    end
  end
  
  # process 'inner radius' bits
  { ir: :r, ir1: :r1, ir2: :r2 }.each do |ir, r|
    if call.named_args.key? ir
      sides = call.named_args[:"$fn"]
      raise "Use of inner_radius requires sides/facets/fragments argument to #{call.method}()" unless sides.is_a? Numeric
      raise "sides/facets/fragments argument must be a whole number (Fixnum)" unless sides.is_a? Fixnum
      raise "sides/facets/fragments argument must be at least 3 #{call} to use inner_radius" unless sides >= 3
      inradius = call.named_args.delete(ir)
      if inradius.is_a? Range
        circumradius = Range.new(inradius.first.to_f / @env.cos(180.0 / sides),
                         inradius.last.to_f / @env.cos(180.0 / sides),
                         inradius.exclude_end?)
      elsif inradius.respond_to? :to_f
        circumradius = inradius.to_f / @env.cos(180.0 / sides)
      else
        raise "#{inradius.inspect} must be Numeric or a Range"
      end
      call.named_args[r] = circumradius
    end
  end
  
  # convert range radius to r1 and r2 pair
  if call.named_args[:r].is_a? Range
    range = call.named_args.delete(:r)
    call.named_args[:r1] = range.first
    call.named_args[:r2] = range.last
  end
end
layout_defaults() click to toggle source

filter to patch in layout defaults like center: true when not specified explicitly in this call

# File lib/oozby/preprocessor-definitions.rb, line 144
def layout_defaults
  # copy in defaults if not already specified
  call.named_args.merge!(@env.defaults) { |k,a,b| a }
end
refuse_args(*list) click to toggle source

ban a list of arguments, to highlight mistakes like passing height to circle Usage> filter :refuse_args, :h

# File lib/oozby/preprocessor-definitions.rb, line 288
def refuse_args *list
  list.each do |name|
    raise "#{@original_method} doesn't support #{name}" if call.named_args.keys.include? name
  end
end
rename_args(pairs) click to toggle source

filter to rename certain arguments to other things Usage> filter :rename_args, :old_arg => :new_arg, :other => morer

# File lib/oozby/preprocessor-definitions.rb, line 151
def rename_args pairs
  pairs.each do |from_keys, to_key|
    from_keys = [from_keys] unless from_keys.is_a? Array
    if from_keys.any? { |key| call.named_args[key.to_sym] }
      value = from_keys.map { |key| call.named_args.delete(key) }.compact.first
      call.named_args[to_key.to_sym] = value if value != nil
    end
  end
end
require_args(*list) click to toggle source

require certain arguments be specified to a processed method Usage> filter :require_args, :first_arg, :second_arg

# File lib/oozby/preprocessor-definitions.rb, line 280
def require_args *list
  list.each do |name|
    raise "#{@original_method} requires argument #{name}" unless call.named_args.keys.include? name
  end
end
resolution() click to toggle source
# File lib/oozby/preprocessor-definitions.rb, line 134
def resolution
  res = @env.resolution
  res.delete_if { |k,v| Oozby::Environment::ResolutionDefaults[k] == v }
  res.each do |key, value|
    call.named_args[(ResolutionNames[key] || "$#{key}").to_sym] ||= value
  end
end
rounded_cylinder(h: 1, r1: 1, r2: 1, center: false, corner_radius: 0) click to toggle source

create a rounded cylinder shape

# File lib/oozby/preprocessor-definitions.rb, line 323
def rounded_cylinder h: 1, r1: 1, r2: 1, center: false, corner_radius: 0
  radii = [r1, r2]
  raise "corner_radius is too big. Max is #{radii.min} for this cylinder" if corner_radius > radii.min
  corner_diameter = corner_radius * 2
  
  preprocessor = self
  # use rounded rect to create the body shape
  capture do
    facets = preprocessor.call.named_args[:"$fn"] || _fragments_for(radius: radii.min)
    
    translate([0,0, if center then -h / 2.0 else 0 end]) >
    #union do
    rotate_extrude(:"$fn" => facets) do
      hull do
        # table to calculate radii at in between y positions
        table = { 0.0 => r1, h.to_f => r2 }
        # offset taking in to account angle of wall, as the line between each
        # circle after the hull operation will not be from exactly corner_radius
        # height when the side angle is not 90deg
        lookup_offset = corner_radius * sin(atan2(r2-r1, h) / 2.0)
        # bottom right corner
        translate([lookup(corner_radius + lookup_offset, table) - corner_radius, h - corner_radius]) >
        circle(r: corner_radius, :"$fn" => facets)
        # top right corner
        translate([lookup(h - corner_radius + lookup_offset, table) - corner_radius, corner_radius]) >
        circle(r: corner_radius, :"$fn" => facets)
        # center point
        square([radii.min - corner_radius, h])
      end
    end
  end
end
rounded_rectangle(size: [1,1], center: false, corner_radius: 0.0, facets: nil) click to toggle source
# File lib/oozby/preprocessor-definitions.rb, line 294
def rounded_rectangle size: [1,1], center: false, corner_radius: 0.0, facets: nil
  size = [size] * 2 if size.is_a? Numeric
  size = [size[0] || 1, size[1] || 1]
  raise "Corner radius is too big. Max #{size.min / 2.0} for this square" if corner_radius * 2.0 > size.min
  corner_diameter = corner_radius * 2.0
  circle_x = (size[0] / 2.0) - corner_radius
  circle_y = (size[1] / 2.0) - corner_radius
  
  capture do
    resolution(fragments: (facets || 0)) do
      translate(if center then [0,0] else [size[0] / 2.0, size[1] / 2.0] end) do
        union do
          square([size[0], size[1] - corner_diameter], center = true)
          square([size[0] - corner_diameter, size[1]], center = true)
          preprocessor true do
            resolution(fragments: (_fragments_for(radius: corner_radius).to_f / 4.0).round * 4.0) do
              translate([ circle_x,  circle_y]) { circle(r: corner_radius) }
              translate([ circle_x, -circle_y]) { circle(r: corner_radius) }
              translate([-circle_x, -circle_y]) { circle(r: corner_radius) }
              translate([-circle_x,  circle_y]) { circle(r: corner_radius) }
            end
          end
        end
      end
    end
  end
end
rounded_rectangular_prism(size: [1,1,1], center: false, corner_radius: 0, facets: nil) click to toggle source

handle rounded cubes

# File lib/oozby/preprocessor-definitions.rb, line 357
def rounded_rectangular_prism size: [1,1,1], center: false, corner_radius: 0, facets: nil
  size = [size] * 3 if size.is_a? Numeric
  size = [size[0] || 1, size[1] || 1, size[2] || 1]
  raise "Radius is too big. Max #{size.min / 2.0} for this cube" if corner_radius * 2.0 > size.min
  corner_diameter = corner_radius.to_f * 2.0

  preprocessor = self
  # use rounded rect to create the body shape
  capture do
    resolution(fragments: (facets || 0)) do
      union do
        offset = if center then [0,0,0] else [size[0].to_f / 2.0, size[1].to_f / 2.0, size[2].to_f / 2.0] end
        translate(offset) do
          # extrude the main body parts using rounded_rectangle as the basis
          linear_extrude(height: size[2] - corner_diameter, center: true) {
            inject_abstract_tree(preprocessor.send(:rounded_rectangle, size: [size[0], size[1]], center: true, corner_radius: corner_radius)) }
          rotate([90,0,0]) { linear_extrude(height: size[1] - corner_diameter, center: true) {
            inject_abstract_tree(preprocessor.send(:rounded_rectangle, size: [size[0], size[2]], center: true, corner_radius: corner_radius)) }}
          rotate([0,90,0]) { linear_extrude(height: size[0] - corner_diameter, center: true) {
            inject_abstract_tree(preprocessor.send(:rounded_rectangle, size: [size[2], size[1]], center: true, corner_radius: corner_radius)) }}

          # fill in the corners with spheres
          xr, yr, zr = size.map { |x| (x / 2.0) - corner_radius }
          corner_coordinates = [
            [ xr, yr, zr],
            [ xr, yr,-zr],
            [ xr,-yr, zr],
            [ xr,-yr,-zr],
            [-xr, yr, zr],
            [-xr, yr,-zr],
            [-xr,-yr, zr],
            [-xr,-yr,-zr]
          ]
          preprocessor true do
            resolution(fragments: (_fragments_for(radius: corner_radius.to_f).to_f / 4.0).round * 4.0) do
              corner_coordinates.each do |coordinate|
                translate(coordinate) do
                  # generate sphere shape
                  rotate_extrude do
                    intersection do
                      circle(r: corner_radius)
                      translate([corner_radius, 0, 0]) { square([corner_radius * 2.0, corner_radius * 4.0], center: true) }
                    end
                  end
                end
              end
            end
          end
        end
      end
    end
  end
end
validate(args = {}) click to toggle source

simple validator to check particular named arguments conform to required types or exactly match a set of values Usage> filter :validate, argument_name: Symbol, other_argument: [“yes”, “no”], radius: [Numeric, Range]

# File lib/oozby/preprocessor-definitions.rb, line 262
def validate args = {}
  args.keys.each do |args_keys|
    acceptable = if args[args_keys].respond_to? :each then args[args_keys] else [args[args_keys]] end
    key_list = if args_keys.respond_to? :each then args_keys else [args_keys] end
    key_list.each do |key|
      # for this key, check it matches acceptable list, if specified
      if call.named_args.keys.include? key
        value = call.named_args[key]
        if acceptable.none? { |accepts| accepts === value }
          raise "#{@original_method}'s argument #{key} must be #{acceptable.inspect}"
        end
      end
    end
  end
end
xyz(default: 0, arg: false, depth: true) click to toggle source

filter calls to a method, transforming x, y, and optionally z arguments in to a 2 or 3 item array, setting it to the argument named ‘arg’ or setting it as the first numerically indexed argument if that is unspecified. A default value can be supplied. Usage> filter :xyz, default: 1, arg: :size, depth: false

# File lib/oozby/preprocessor-definitions.rb, line 234
def xyz default: 0, arg: false, depth: true
  if [:x, :y, :z].any? { |name| call.named_args.include? name }
    # validate args
    [:x, :y, :z].each do |key|
      if call.named_args.has_key? key
        unless call.named_args[key].is_a? Numeric
          raise "#{key} must be Numeric, value #{call.named_args[key].inspect} is not."
        end
      end
    end
    
    coords = [call.named_args.delete(:x), call.named_args.delete(:y)]
    coords.push call.named_args.delete(:z) if depth
    coords.map! { |x| x or default } # apply default value to missing data
    
    # if argument name is specified, use that, otherwise make it the first argument in the call
    if arg
      call.named_args[arg] = coords
    else
      call.args.unshift coords
    end
  end
end