wicket 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a1d5db24047874cba578ee40d7b03c7ec2e204f5
4
- data.tar.gz: 70c327e69d7a54897fa73436eb13b5d175a5711c
3
+ metadata.gz: bd072dfef6d4eb66f52974a60e5b6a1df82c24a3
4
+ data.tar.gz: a3e29991b47184b46eff7801a3678a7dc7c4d2fd
5
5
  SHA512:
6
- metadata.gz: 00f66f02b83314f43d2567e7c2324223dd0d769036c27bf9571896330ea485703724a0c3a5d155ed66cab9757c8c7f567cc1f490dcfb474cd9af0739769e8243
7
- data.tar.gz: 65885c4b9f665eee96d41cf70ecf1e6587befb6fe64612849df94a281d12198804d6f966a669569a545b45871dbc632a625adefe900fe03057a9f6fdbcd4a047
6
+ metadata.gz: d22c55c1e9ade2cc8d48e7d5c2a579e5e88f980b81dc7ae67a3ee89a58260eefa49b5828a163dd09a89906162dfaf33907b5307ffdc0593aadb0c98523a24792
7
+ data.tar.gz: b38949f08bd4eaaa4966cb54f8dbe36ab48693edef333373d558a36418e77e22bd5b9051edb6778960384782e4b4e183e801f908d655b52205b24e119007e9f0
data/.travis.yml CHANGED
@@ -1,11 +1,8 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.1.0
3
+ - 2.1
4
4
  - 2.0.0
5
5
  - 1.9.3
6
- - 1.8.7
7
- - jruby-18mode
8
6
  - jruby-19mode
9
- - rbx
10
7
  - ruby-head
11
- script: rspec
8
+ script: rspec
data/Gemfile CHANGED
@@ -2,3 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in wicket.gemspec
4
4
  gemspec
5
+
6
+ gem "pry"
data/README.md CHANGED
@@ -8,17 +8,20 @@ Notable features include:
8
8
  - Translating line paths into WKT Polygons or Multipolygons
9
9
  - Accepting absolute or relative path commands
10
10
  - Inversing Y axis measurements (SVG y coordinates decrease as you go up)
11
+ - Translates quadratic and cubic curve commands into straight line segments
12
+ - Curve definition translation controls
13
+ - Numeric precision controls
11
14
  - Decimal math for increased accuracy
12
15
 
13
16
  Future possible features could include:
14
- - Translating curved paths
17
+ - The arc "A" command
15
18
  - Translating non closed paths into Linestrings and Multilines
16
19
  - Specifying paths for polygons which are exlcusions from the area (donut holes)
17
20
  - Translating WKT to SVG (although PostGIS already does this)
18
21
 
19
22
  If you want to see these or any other features, feel free to make an issue or pull request.
20
23
 
21
- Wicket is similar in concept to [SVG-to-WKT.js](https://github.com/davidmcclure/svg-to-wkt), although that project operates within the browser and focuses much more on the DOM elements. It also owes credit to [Savage](https://github.com/awebneck/savage) as a reference for the SVG parser. If you need to edit SVG paths, I would recommend checking it out.
24
+ Wicket is similar in concept to [SVG-to-WKT.js](https://github.com/davidmcclure/svg-to-wkt), although that project operates within the browser. It also owes credit to [Savage](https://github.com/awebneck/savage) as a reference for the SVG parser. If you need to edit SVG paths, I would recommend checking it out.
22
25
 
23
26
  ## Installation
24
27
 
@@ -34,35 +37,152 @@ Or install it yourself as:
34
37
 
35
38
  $ gem install wicket
36
39
 
40
+ ## Configuration
41
+
42
+ You can pass configuration parameters to Wicket at three times:
43
+
44
+ - At load time, using an initializer
45
+
46
+ ```ruby
47
+ Wicket.configure do |w|
48
+ w.min_angle = 165
49
+ w.min_angle_unit = :degrees
50
+ end
51
+ ```
52
+
53
+ - At instantiation time (overrides the initializer)
54
+
55
+ ```ruby
56
+ Wicket::SVGPath.new("M10 10H20l10 10H10z",
57
+ min_angle: 165,
58
+ min_angle_unit: :degrees
59
+ )
60
+ ```
61
+
62
+ - At conversion time (overrides the initializer and instantiation)
63
+
64
+ ```ruby
65
+ path = Wicket::SVGPath.new("M10 10H20l10 10H10z")
66
+ path.to_polygon(min_angle: 165, min_angle_unit: :degrees)
67
+ ```
68
+
69
+ ### Configuration Options
70
+
71
+ There are currently two configuration options:
72
+
73
+ 1. `min_angle`
74
+
75
+ This is the minimum angle allowed when translating a curve into a series of straight lines. A higher minimum will require more line segments, more vertices, and more text, but it will get you a more accurate line.
76
+
77
+ There are 4 ways to describe an angle to Wicket, and luckily Wicket is pretty smart about figuring out which one you want, although, as noted below, you can explicitly state which one you want.
78
+
79
+ 1. `decimal_percentage`
80
+
81
+ If the `min_angle` is greater than 0 and less than 1, Wicket assumes you are expressing a decimal percentage of the maximum total angle (180 degrees, or Pi radians). For example, 0.9 would indicate that you only want to allow angles of 0.9 * Pi radians or 0.9 * 180 degrees.
82
+
83
+ 2. `radians`
84
+
85
+ If the `min_angle` is greater than or equal to 1 and less than Pi, it will assume you are talking in radians.
86
+
87
+ 3. `percentage`
88
+
89
+ If the `min_angle` is greater than or equal to Pi, and less than 100 it will assume you are talking in percentage terms. This is the same as #1 except multiplied by 100.
90
+
91
+ 4. `degrees`
92
+
93
+ If the `min_angle` is greater than or equal to 100, but less than 180, it will assume you are talking in degrees.
94
+
95
+ Obviously, this must be a numeric value greater than 0 and less than 180.
96
+
97
+ 2. `min_angle_unit`
98
+
99
+ As noted above, this should rarely be necessary, but sometimes it can be included for explcitness in the code, or in rare cases, used to override Wicket's guess as to what unit you are using. For example, if you for some reason wanted the `min_angle` to be 0.5 radians, you could explicitly state so and Wicket would comply. This would, however, result in a pretty poorly fit curve, since the above guesses cover all obtuse angles in all four formats.
100
+
101
+ This option accepts the same four arguments as above: `decimal_percentage`,`radians`, `percentage`, and `degrees`
102
+
103
+ 3. `scale`
104
+
105
+ This is the number of digits allowed after the decimal point, exactly as you would in a Rails migration for a decimal column. Note that this is only used for formatting, all calculations are done to the maximum possible precision.
106
+
107
+ If scale is omitted, Wicket will use the greatest scale of all the input points provided. Thus:
108
+
109
+ ```
110
+ "M0,0H100V100H0z" # => 0 scale
111
+ "M0,0H100V100H0.0z" # => 1 scale
112
+ "M0,0H100V100H0.00z" # => 2 scale
113
+ "M0.000,0H100V100H0.00z" # => 3 scale
114
+ ```
115
+
37
116
  ## Usage
38
117
 
118
+ ### Linear paths
39
119
  ```ruby
40
120
  # one subpath
41
121
  path = Wicket::SVGPath.new("M10 10H20l10 10H10z")
42
122
  path.to_polygon
43
- # => "POLYGON((10.0 -10.0,20.0 -10.0,30.0 -20.0,10.0 -20.0,10.0 -10.0))"
123
+ # => "POLYGON((10 -10,20 -10,30 -20,10 -20,10 -10))"
44
124
  path.to_multipolygon
45
- # => "MULTIPOLYGON(((10.0 -10.0,30.0 -10.0,40.0 -10.0,50.0 -20.0,40.0 -20.0,10.0 -10.0)))"
125
+ # => "MULTIPOLYGON(((10 -10,20 -10,30 -20,10 -20,10 -10)))"
46
126
 
47
127
  # two subpaths
48
128
  path = Wicket::SVGPath.new("M10 10H20l10 10H10z M100 100h10v10h-10z")
49
129
  path.to_polygon # ONLY THE FIRST SUBPATH!
50
- # => "POLYGON((10.0 -10.0,20.0 -10.0,30.0 -20.0,10.0 -20.0,10.0 -10.0))"
130
+ # => "POLYGON((10 -10,20 -10,30 -20,10 -20,10 -10))"
131
+ path.to_multipolygon # both subpaths
132
+ # => "MULTIPOLYGON(((10 -10,30 -10,40 -10,50 -20,40 -20,10 -10)),((100 -100,110 -100,110 -110,100 -110,100 -100)))
133
+
134
+ ```
135
+
136
+ ### Curved paths
137
+ ```ruby
138
+ # cubic
139
+ path = Wicket::SVGPath.new("M100 100,c0 200,200 200,200 0z")
140
+ path.to_polygon
141
+ # => "POLYGON((100 -100,102 -135,108 -165,131 -212,146 -228,163 -240,181 -247,200 -250,218 -247,236 -240,253 -228,268 -212,281 -191,291 -165,300 -100,100 -100))"
142
+ path.to_multipolygon
143
+ # => "MULTIPOLYGON(((100 -100,102 -135,108 -165,131 -212,146 -228,163 -240,181 -247,200 -250,218 -247,236 -240,253 -228,268 -212,281 -191,291 -165,300 -100,100 -100)))"
144
+
145
+ # quadratic
146
+ path = Wicket::SVGPath.new("M10 10,Q110 210 210 10z")
147
+ path.to_polygon
148
+ # => "POLYGON((10 -10,35 -53,60 -85,72 -95,85 -103,97 -108,110 -110,122 -108,135 -103,147 -95,160 -85,185 -53,210 -10,10 -10))"
51
149
  path.to_multipolygon # both subpaths
52
- # => "MULTIPOLYGON(((10.0 -10.0,30.0 -10.0,40.0 -10.0,50.0 -20.0,40.0 -20.0,10.0 -10.0)),((100 -100,110 -100,110 -110,100 -110,100 -100)))
150
+ # => "MULTIPOLYGON(((10 -10,35 -53,60 -85,72 -95,85 -103,97 -108,110 -110,122 -108,135 -103,147 -95,160 -85,185 -53,210 -10,10 -10)))
151
+
152
+ ```
153
+
154
+ ## Debugging/Proof
155
+
156
+ Because this is all pretty hard to keep track of in your head, you can call `#to_svg` on any path, and it will output a SVG of your original path in blue and a representation of the WKT in green for visual comparison, ready to paste into a page or JSFiddle or something. You may have to adjust the viewbox to make sure the path is visible. `#to_svg` takes all the same parameters as `#to_polygon`.
157
+
158
+ ```ruby
159
+ path = Wicket::SVGPath.new("M0 0,C0 200,200 200,200 0z")
160
+ puts path.to_svg
161
+ # <svg>
162
+ # <path d="M0 0,C0 200,200 200,200 0z" style="fill: slateblue; opacity:0.2"/>
163
+ # <path d="M0,0 L 2,35 8,65 31,112 46,128 63,140 81,147 100,150 118,147 136,140 153,128 168,112 181,91 191,65 200,0 Z" style="fill:none;stroke:lawngreen" stroke-weight="2"/>
164
+ # </svg>
53
165
 
54
166
  ```
55
167
 
56
168
  ## Gotchas
57
169
 
58
- - Wicket assumes that a move command (M or m) is part of the polygon edge. It just so happened that a lot of the data I was working with when creating this project was formatted that way. Eventually I believe this should not be the case and should be able to be toggled on as an option.
170
+ - Be careful when using curves that you have an appropriate scale set for the magnitude of the numbers you are using. If you are dealing with small numbers, curves, and your inputs are integers, all the points will be rounded to integers. You probably don't want that.
171
+
172
+ ```ruby
173
+ path = Wicket::SVGPath.new("M0,0Q1,1,2,0z")
174
+ path.to_polygon # probably not what you want
175
+ # => "POLYGON((0 0,0 0,0 0,0 0,1 0,1 0,1 0,1 0,2 0,0 0))"
176
+ path.to_polygon(scale: 4) # much better
177
+ # => "POLYGON((0.0 -0.0,0.25 -0.2188,0.5 -0.375,0.75 -0.4688,1.0 -0.5,1.25 -0.4688,1.5 -0.375,1.75 -0.2188,2.0 -0.0,0.0 -0.0))"
178
+ ```
179
+ - Wicket assumes that a move command (M or m) is part of the polygon edge. It just so happened that a lot of the data I was working with when creating this project was formatted that way. If your cursor moves around a lot and does not intend to define these as edges, be aware.
59
180
  - Polygons are assumed to have no holes, thus everything inside the path is part of the polygon.
60
181
  - Each polygon is represented by a subpath. A subpath continues until it is closed by a Z command. Multiple subpaths are represented as individual elements in `#to_multipolygon`, whereas only the first subpath is represented in `#to_polygon`. Make sure your data does not include multiple subpaths if using the `#to_polygon` method.
182
+ - "-0.0" is a possible value, [as negative zero is a concept in BigDecimal](http://www.ruby-doc.org/stdlib-1.9.3/libdoc/bigdecimal/rdoc/BigDecimal.html#class-BigDecimal-label-Positive+and+negative+zero).
61
183
 
62
184
  ## Contributing
63
185
 
64
- 1. Fork it ( http://github.com/rurabe/wicket/fork )
65
- 2. Create your feature branch (`git checkout -b my-new-feature`)
66
- 3. Commit your changes (`git commit -am 'Add some feature'`)
67
- 4. Push to the branch (`git push origin my-new-feature`)
68
- 5. Create new Pull Request
186
+ If you have a path that you think is being mistranslated, please submit a pull request with your path added to `spec/test_cases.yml`, and if possible, the fix.
187
+
188
+ Otherwise, submit a pull request for any other feature.
data/changelog.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## 0.0.3
4
+ <!-- - Converts C commands -->
5
+
6
+ ## 0.0.2
7
+ - Converts Z, L, H, and V commands
8
+ - Added README
9
+
10
+ ## 0.0.1
11
+ - Initial release
12
+ - Converts M commands
data/lib/wicket.rb CHANGED
@@ -1,13 +1,35 @@
1
1
  require "bigdecimal"
2
+ require 'bigdecimal/util'
2
3
  require "wicket/version"
4
+ require "wicket/utilities"
5
+ require "wicket/cartesian"
6
+ require "wicket/configuration"
7
+ require "wicket/coordinate"
8
+ require "wicket/subpoint"
3
9
  require "wicket/svg_path"
4
10
  require "wicket/subpath"
11
+ require "wicket/commands/bezier_curve"
12
+ require "wicket/commands/cubic_bezier"
13
+ require "wicket/commands/quadratic_bezier"
5
14
  require "wicket/command"
6
15
  require "wicket/commands/m"
7
16
  require "wicket/commands/l"
8
17
  require "wicket/commands/z"
9
18
  require "wicket/commands/h"
10
19
  require "wicket/commands/v"
20
+ require "wicket/commands/c"
21
+ require "wicket/commands/s"
22
+ require "wicket/commands/q"
23
+ require "wicket/commands/t"
24
+
11
25
 
12
26
  module Wicket
27
+ class << self
28
+ attr_reader :configuration
29
+ def configure
30
+ @configuration ||= Configuration.new
31
+ yield(@configuration)
32
+ end
33
+ end
34
+ configure {}
13
35
  end
@@ -0,0 +1,23 @@
1
+ module Wicket
2
+ module Cartesian
3
+
4
+ def distance_to(remote)
5
+ Math.sqrt((remote.x - x) ** 2 + (remote.y - y) ** 2)
6
+ end
7
+
8
+ def to_wkt(o={})
9
+ [format(x,o),format((y * -1),o)].join(" ")
10
+ end
11
+
12
+ def to_svg(o={})
13
+ [x,y].map{|a| format(a,o)}.join(",")
14
+ end
15
+
16
+ private
17
+
18
+ def format(n,opts={})
19
+ Utilities.format(n,opts)
20
+ end
21
+
22
+ end
23
+ end
@@ -1,45 +1,78 @@
1
1
  module Wicket
2
2
  class Command
3
- attr_reader :cursor_start
3
+ attr_reader :cursor_start, :x, :y
4
4
 
5
5
  class << self
6
- def from_code(code,arg_string,subpath)
6
+ def from_code(code,arg_string,subpath,opts={})
7
7
  command_class = Commands.const_get(code.upcase) # get the class which handles this code
8
8
  absolute = (code.upcase == code) # is it uppercase?
9
9
  cursor_start = subpath.cursor_end # get the current position of the cursor
10
10
  args = arg_string.to_s.scan(/(?:\-\s*)?\d+(?:\.\d+)?/).flatten # parse out the numerical arguments
11
11
  if !args.empty?
12
- generate_commands(args,command_class,absolute,cursor_start)
12
+ set_scale!(args,opts)
13
+ generate_commands(args,command_class,absolute,cursor_start,subpath,opts)
13
14
  else # Must be a Z command
14
- [command_class.new(absolute,cursor_start)]
15
+ [command_class.new(absolute,cursor_start,subpath,opts)]
15
16
  end
16
17
  end
17
18
 
18
19
  private
19
20
 
20
- def generate_commands(args,command_class,absolute,cursor_start)
21
- args.each_slice(command_class.arg_count).map do |slice| # slice them according to the number the code takes
21
+ def generate_commands(args,command_class,absolute,cursor_start,subpath,opts={})
22
+ args.each_slice(command_class::ARGS).map do |slice| # slice them according to the number the code takes
22
23
  slice.map!{|arg| BigDecimal.new(arg.gsub(/\s*/,'')) } # remove whitespace and turn into a decimal
23
- command_class.new(absolute,cursor_start,*slice)
24
+ command_class.new(absolute,cursor_start,subpath,opts,*slice)
24
25
  end
25
26
  end
27
+
28
+ def set_scale!(args,opts={})
29
+ opts[:_scale] = [opts[:_scale].to_i,args.map{|s| d = s.split(".")[1]; d ? d.length : 0 }.max].max
30
+ end
26
31
  end
27
32
 
28
33
  def subpath=(subpath)
29
34
  @subpath = subpath
30
35
  end
31
36
 
32
- def inspect
33
- "#<#{self.class.to_s} #{coordinates.map{|k,v|[k,v].join("=")}.join(",")}#{" abs" if @absolute}>"
37
+ def absolute_x
38
+ @absolute ? x : cursor_start.x + ( @x || 0 )
34
39
  end
35
40
 
36
- def to_wkt
37
- inverse_values.join(" ")
41
+ def absolute_y
42
+ @absolute ? y : cursor_start.y + ( @y || 0 )
38
43
  end
39
44
 
40
- private
41
- def inverse_values
42
- [cursor_end[:x].to_s("F"), (cursor_end[:y] * -1).to_s("F")]
45
+ def cursor_end
46
+ Coordinate.new(absolute_x,absolute_y)
47
+ end
48
+
49
+ def to_wkt(opts={})
50
+ o = @opts.merge(opts)
51
+ cursor_end.to_wkt(o)
52
+ end
53
+
54
+ def to_svg(opts={})
55
+ o = @opts.merge(opts)
56
+ [letter,cursor_end.to_svg(o)].join("")
57
+ end
58
+
59
+ def arguments
60
+ self.class::ARG_LIST.each_with_object({},&arg_maker)
61
+ end
62
+
63
+ private
64
+
65
+ def letter
66
+ self.class.to_s.match(/\:\:([^\:]+)$/)[1]
67
+ end
68
+
69
+ def arg_maker
70
+ Proc.new do |att,hash|
71
+ hash.merge!(att => instance_variable_get("@#{att}"))
43
72
  end
73
+ end
74
+
75
+
76
+
44
77
  end
45
78
  end
@@ -0,0 +1,75 @@
1
+ module Wicket
2
+ module Commands
3
+ module BezierCurve
4
+
5
+ def evaluate_curve(t)
6
+ x = de_casteljau(:x,t,*control_points)
7
+ y = de_casteljau(:y,t,*control_points)
8
+ [x,y,t]
9
+ end
10
+
11
+ def to_wkt(opts={})
12
+ o = @opts.merge(opts)
13
+ subpoints(o).reject{|s| s.t == 0 }.map{|s| s.to_wkt(o) }.join(",")
14
+ end
15
+
16
+ def to_svg(opts={})
17
+ o = @opts.merge(opts)
18
+ "L #{subpoints(o).reject{|s| s.t == 0}.map{|s| s.to_svg(o) }.join(" ")}"
19
+ end
20
+
21
+ private
22
+
23
+ def subpoints(opts)
24
+ points = init_subpoints
25
+ smooth(points,new_point(points,*points),opts)
26
+ points.sort_by(&:t)
27
+ end
28
+
29
+ def de_casteljau(axis,t,*points)
30
+ n = (points.length - 1)
31
+ points.each_with_index.inject(0) do |m,(p,i)|
32
+ m + ( choose(n,i) * ((1-t) ** (n-i)) * (t ** i) * p.send(axis))
33
+ end
34
+ end
35
+
36
+ def choose(n,k)
37
+ (0...k).inject(1){ |m,i| (m * (n - i)) / (i + 1) }.to_d
38
+ end
39
+
40
+ def smooth(points,point,opts)
41
+ if point_needed?(point,opts)
42
+ smooth(points,new_point(points,point,point.previous_neighbor),opts)
43
+ if point_needed?(point,opts)
44
+ smooth(points,new_point(points,point,point.next_neighbor),opts)
45
+ end
46
+ end
47
+ end
48
+
49
+ # is the angle more than the tolerance?
50
+ def point_needed?(point,opts)
51
+ point.angle < Utilities.radians_tolerance(opts)
52
+ end
53
+
54
+ def new_point(points,point,neighbor)
55
+ x,y,t = evaluate_curve((point.t + neighbor.t) / 2.0)
56
+ Subpoint.new(x,y,t,points)
57
+ end
58
+
59
+ def init_subpoints
60
+ [].tap do |a|
61
+ Subpoint.from_coordinate(@cursor_start,0,a)
62
+ Subpoint.from_coordinate(cursor_end,1,a)
63
+ end
64
+ end
65
+
66
+ def set_implicit_control_point!
67
+ if @absolute
68
+ @c1x,@c1y = implied_c1.x,implied_c1.y
69
+ else
70
+ @c1x,@c1y = @cursor_start.relativize(implied_c1)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end