Returning a value from a Class method
-
To try to clarify and simplify the Ruby Tool I'm developing, I've developed a new PointsArray class, with several methods for handling arrays of Sketchup points.
The methods work fine in the Ruby console, and seem to do what I intend.
One method, for example, shifts a rectangular PointsArray in the Y direction, then rotates it 90 degrees about the Z axis.
But when I use an assignment in my Ruby code, it picks up an unchanged value from the original array:
# @profile_pointsH is a PointsArray of points in a rectangle with one corner cut off, wider than it is high. @profile_pointsH = PointsArray.new @profile[2..-1] @profile_pointsV = @profile_pointsH.shiftYrotateZ90
The output of puts @profile_pointsV.contents.inspect show that it has not changed orientation or position from the horizontal profile. (The attr_accessor is called :contents)
The Class method is:
def shiftYrotateZ90 ## Transform to shift down by thickness and rotate 90 degrees to turn from horizontal to vertical orientation ## Calculate width and thickness ## width = @contents.max.x - @contents.min.x thickness = @contents.max.y - @contents.min.y ## Define transform to shift down shiftY = Geom;;Transformation.translation(Geom;;Vector3d.new 0.0, -thickness, 0.0) ## Define transform to rotate 90 degrees about Z_AXIS rotateZ90 = Geom;;Transformation.rotation [0,0,0],Z_AXIS, 90.degrees ## Combine and transform from horizontal to vertical temp = self @contents.each_index {|i| temp[i] = @contents[i].transform (rotateZ90*shiftY)} puts temp.inspect temp end
I've checked that the transforms in the method are correct, and indeed have verified that the values in temp.inspect show that the method HAS shifted and rotated the rectangle, and I understood that the method 'returns' the last line (the value of temp) as its value.
I thought that this value would then be assigned to the @profile_pointsV object by the = operation.
It IS created as a PointsArray but contains just the same points in the same order as the original @profile_pointsH PointsArray, and I can't see why.
I'm missing something in my understanding of how Ruby works. Why does this give the expected output when I copy the method directly into the Ruby Console and run the assignment with values for @profile_pointsH, but not in SU code?
Sorry if I'm being dense, but at the moment, I'm just not seeing where it's wrong.
Any help would be most welcome - I'm feeling stuck at this point.
-
In this method you're returning
self
. In this method, you're changing@profile_pointsH
and linking same array to to another variable@profile_pointsV
. I.E as of result both variables point to the same object.You have to change
temp = self
totemp = self.clone
or simply create a new PointsArray instance, liketemp = PointsArray.new()
. -
Or to put it another way...
Some class references - like Array - don't make a copy of the original, but still point at the original object - so any changes made to the copy will affect the original too - you sidestep the issue you need to make the copy using .clone [or .dup] - it's then a new separated Array.Simple examples:
Real copies of simply assigned values...
x=1 y=x y=2 x returns 1 y returns 3
Arrays...
a=[1, 2] b=a b[0]=3 b returns [3, 2] a returns [3, 2]
Cloned...
a=[1, 2] b=a.clone b[0]=3 b returns [3, 2] a returns [1, 2]
-
Thank you again, TIG, for your clarification. Unfortunately, the suggested solutions don't seem to make any difference.
I was aware that a copy of an array is just a copy of the pointer to the contents, and as you describe, that a clone has separate contents.
But I've tried
temp = self.clone,
andtemp = PointsArray.new
inside the method, and still get the ..V (supposedly transformed) arrays remaining the same as the (unchanged)..H original. I even reloaded SU in case something was being cached in the Ruby console even after I reloaded the .rb file.I've also tried
@profile_pointsV = @profile_pointsH.clone.shiftYrotateZ90
and@profile_pointsV = @profile_pointsH.copy.shiftYrotateZ90
in the calling code, (where.copy
is a method I wrote that does an element by element copy, leaving the original unchanged), both with and without having changed the 'temp = ...
' line in the method.And I tried changing
temp = self
totemp = self.clone
or totemp = PointsArray.new
in both this and other functions like flip the array about its centre along an axis: still no difference to the result: the assigned value is the same as the unchanged original when I call the method from the mainline Tool code, and there are no console errors.def flip(axis) ## Flip the array along the specified axis about its centre ## Find the centre centrepoint = Geom;;Point3d.linear_combination(0.5, @contents.min, 0.5, @contents.max) puts "centrepoint = " + centrepoint.to_s temp = self.clone # originally just temp = self ## Scale the array around the centrepoint to reverse the values along specified axis flip = Geom;;Transformation.scaling(centrepoint, -axis.x,-axis.y,-axis.z) @contents.each_index {|i| temp[i] = @contents[i].transform(flip)} return temp end
Nothing seems to work to perform the transform when run in the Tool code, but it DOES (bizarrely) seem to work when copied and pasted on its own into the Ruby console as a function, and called from there, as far as I can see.
I didn't remember to mention it before, but both my
class DrawFraming
and theclass PointsArray < Array
are contained in the same file and wrapped in a module JWM, so the PointArray.new
method, and the other methods should be (and certainly seem to be) accessible from within DrawFraming.Is there something wrong with the def initialize method?
class PointsArray < Array ## Methods for manipulating arrays of points attr_accessor ;contents def initialize(*args) @contents = args[0] end
If so, what is wrong, and what should it be instead?
I'm still stumped.
Any further thoughts for me to pursue?
I may have to go back to rewriting them just as functions in the DrawFraming class, and call them with parameters, but I was hoping to learn more about object oriented programming and using them (
PointsArray
s) as new kinds of objects.PS. Progress of a kind. But as well as changing the target ..V variable, it also seems to change the original ..H one.
If I change the code to:
... temp = self.clone @contents.each_index {|i| temp.contents[i] = @contents[i].transform (rotateZ90*shiftY)} temp end
(where the change is to assign
temp.contents[i]
instead oftemp[i]
), at least the transform works to change the arrays. But it also changes the original ..H PointsArray - and I can't find a way of separating the two!I even tried calling:
@profile_pointsV = @profile_pointsH.clone @profile_pointsV.shiftYrotateZ90
and
@profile_pointsV = @profile_pointsH.copy @profile_pointsV.shiftYrotateZ90
but I STILL can't seem to separate the ..V and ..H variables' contents. If one shifts and rotates, so does the other.
Is
temp = self.clone
using self to refer to the class, or to the instance, of PointsArray?I tried
temp = PointsArray.new()
but that doesn't work either - gives a Ruby error when the method it called. Doesn't like the () - without that, it doesn't error, but doesn't work either.Now even more confused!
-
I'm now beginning to wonder if in some way my approach of defining a new
class PointsArray
is heading me down a blind alley.Would I do better just to define some extra methods for the
class Array
?As in, for example:
class Array def newMethod # code for new method here, creating a new array newArray from a clone of the old one ... return newArray end
Might there might be fewer traps for the unwary in that approach?
I've already done the equivalent - extending an existing Class - (using code slightly adapted from Martin Rinehart's From Edges to Rubies) to add a method
.to_matrix
to the Geom::Transformation class, to output the transformation in a 4 x 4 matrix format that as well as displaying the Transformation matrix in human-readable form can also be read in as an array of arrays - altering Martin's code to insert square brackets and comma element separators appropriately. -
thickness = @contents.max.y - @contents.min.y
Does this actually work for you? I get this error, Error: #<ArgumentError: comparison of Geom::Point3d with Geom::Point3d failed>
-
I'd willing to have a look. Do you have the current source somewhere we can get at?
-
@sdmitch said:
thickness = @contents.max.y - @contents.min.y
Does this actually work for you? I get this error, Error: #<ArgumentError: comparison of Geom::Point3d with Geom::Point3d failed>
It does, for me. It was intended to select out the y coordinate (a scalar) from the point coordinates.
puts @contents[0].class
outputs**Array**
. Each point coordinate seems to be stored as a 3-element array, not a Point3d object - not really a surprise, since I defined and use them that way most of the time. I haven't needed so far to have a 'real' Point3d, but I would convert to one withGeom::Point3d.new(x, y , z)
.If you are storing Point3d objects, would you need
@contents.to_a
first, before.max.y
ormin.y
?I wasn't sure if the
.x
and.y
methods would work on arrays, but they seem to in my example code.I'm unclear about the near-but-not-complete equivalence between SU
Point3d(x, y, z)
andArray[x, y, z]
- it seems I can use either a lot of the time, but not quite always!As you may have already gathered, though, I remain a relative novice in understanding the innards of Ruby, let alone SU Ruby! I mostly try to adapt others' code examples, thought I'm now trying much harder to understand why and how they work - with mixed results, as you see from my questions here!
-
May I inject a pause, for reflection...
If you explained [in words - NOT code] what it is you are trying to do, then we might better advise you...
My own scripts do many convoluted things, but I have never seen the necessity to write a whole new 'Point' class to do something...
I am sure that whatever it is you plan to do is quite possible, and probably considerably less complicated than you have so far contrived it to be...Happy New Year...
-
@jim said:
I'd willing to have a look. Do you have the current source somewhere we can get at?
Jim, that's an extremely generous offer, and I'd love you to look and comment.
My current source is a mess at the moment. Since I'm in the middle of rewriting it to try and change from in-line calculations to object based functions, and also to work with asymmetrical profiles, some of the later executed code is still 'old stuff', inconsistent, and can error distractingly.
Given another day or so, I could probably comment out the error-generating bits, and either attach a file here, or put the latest version on GitHub, where I have earlier versions too - see
Commits · johnwmcc/draw_framing
Sketchup ruby to draw timber framing in standard UK softwood sizes - Commits · johnwmcc/draw_framing
GitHub (github.com)
I'm still trying to figure out branching in GitHub - and haven't even started any branches as a result.
V 0.70 is the latest uploaded version, and works usably for symmetrical (rectangular) profiles. I have part-working but pre-object based versions that almost work for non-rectangular profiles saved locally - it was in trying to improve these that I thought it would be simpler to start over with PointsArray objects!
I had got in a horrible tangle trying to keep track of the profile rotation (in any of four quadrants), orientation (horizontal or vertical), flip state (basic profile shape can be flipped along Y, X, or both X and Y directions, or back to original), getting components drawn at the origin with axes aligned to the sides, and transforming both drawn geometry and created geometry to the original pickpoint and (if picked on a face) the face normal!
It's possible I'll have time to get a tidied up version that doesn't error unnecessarily by the end of this evening (UK time), but it's more likely it won't be until tomorrow - I'll post here again when I've done it.
Thanks again - the help on this forum is very generous, and I thank you both in retrospect and in advance.
PS. Happy New Year!
My posts here get stamped an hour later than I make them - perhaps I'm regarded as still on British Summer Time? We're back to GMT in the winter, so it isn't quite so nearly midnight as it may appear, but not long.
-
I figured that was the case regarding the points not being Point3d's
For me
@profile_pointsH = PointsArray.new @profile[2..-1] doesn't return anything. I assume because the class initialize merely saves @profile[2..-1] in @contents.@profile_pointsV = @profile_pointsH.shiftYrotateZ90 returns the transformed values since it processes @contents and returns temp.
-
@sdmitch said:
@profile_pointsH = PointsArray.new @profile[2..-1] doesn't return anything. I assume because the class initialize merely saves @profile[2..-1] in @contents.
@profile_pointsV = @profile_pointsH.shiftYrotateZ90 returns the transformed values since it processes @contents and returns temp.
Sorry, @profile[2..-1] contains the values:
[[0, 0, 0], [0.875, 0, 0], [1.75, 0.4375, 0], [1.75, 0.875, 0], [0, 0.875, 0], [0, 0, 0]]which is a nominal 2" x 1" timber, with one corner planed off mid-line to mid-line.
I get quite sensible results when I run the code direct in the console as a function. But not when run in the tool.
Will post later a cleaned up version of the current non-working code as suggested by Jim.
-
Have now put a simplified but non-erroring partially working copy of draw_framing.rb and changelog.txt on GitHub as v0.7.2.2 at
Commits · johnwmcc/draw_framing
Sketchup ruby to draw timber framing in standard UK softwood sizes - Commits · johnwmcc/draw_framing
GitHub (github.com)
I've also attached an installable .rbz file to this post. If you install that, you'll get the other needed files to make the draw_framing.rb code work - icon files, etc - and also some earlier versions copied into Plugins, but not loading.
Run it inside Ruby console in SketchUp to see the behaviour I'm trying to fix - the V and H profiles always move together - V is drawn in 3px wide orange and H is 1px black.
Mouse movement should (but doesn't) rotate the profile smoothly round the first pick point changing profile rotation quadrant, and orientation (width horizontal or vertical) as you go. I HAVE had that working (in v0.7.0 if you'd like to try it) for a rectangular profile.
J
Installable v0.7.2.2 with other earlier versions included in file
-
I think I got it:
Change PointsArray constructor to this:
def initialize(*args) @contents = [] if args[0].is_a?(Array) for i in 0...args[0].size @contents[i] = args[0][i].dup end end end
Change
temp.clone
in shiftYrotateZ90 totemp = PointsArray.new()
-
That's cracked it! At last, the @profile_pointsH and @profile_pointsV have separate contents.
And in retrospect, I can see why this works. Though not quite so sure (yet anyway) why the original PointsArray didn't.
Thank you so much.
Now I can get on with making the rest of it work again - I've had almost all the parts working at one time or another, but not using PointsArray - except for this last part, of getting the rotation and flipping to work together - one or the other but never both.
-
@tig said:
:shock:
May I inject a pause, for reflection...
If you explained [in words - NOT code] what it is you are trying to do, then we might better advise you...
My own scripts do many convoluted things, but I have never seen the necessity to write a whole new 'Point' class to do something...
I am sure that whatever it is you plan to do is quite possible, and probably considerably less complicated than you have so far contrived it to be...Happy New Year...
TIG, a very helpful pause - I do get carried away, and I do tend sometimes to over-complicate! Your post crossed with my reply to Jim.
I want to be able to draw frames for woodwork construction (mainly for our local amateur theatre stage scenery), using standard sizes of timber. Here's an example:
I have a working program (see last reply to Jim) which does this for rectangular timber. I'm trying to extend it to work with non-rectangular profile timber, and instead of PushPulling to length, to FollowMe round a selected path.
In parallel with this aim, I'm also trying to teach myself better coding in Ruby, in particular, using objects with methods - IF it turns out to be appropriate (it may not, in this instance, as you rightly point out - but I did enjoy trying, anyway!)
The drawing process in SU is:
-
Use a Right Click to select the (nominal) timber size you want to use from a menu, or specify an actual custom size. Nominal sizes are 1/8th inch (3mm) or 1/4 inch (6mm) larger than the finished size. Remember the choice during the SU session, or until it is changed again.
-
Pick a point, optionally on a face, optionally with an axis locked to X, Y, or Z_AXIS (toggle selected by cursor key). The timber's long axis (normal to its cross sectional profile) will be along the face normal if a face was picked, or the pre-selected axis if one was locked, or otherwise the Z_AXIS.
-
Move or drag the mouse round the point, to set the direction of the timber profile with width horizontal or vertical, in any of four quadrants around the point. *If the camera view is looking 'down' the face normal or axis lock, then change the rotation so it still goes the right way round on screen, even though it is 'looking' at the 'back' of the face or axis.
-
Use the TAB key to cycle the flip state of the profile (only useful for non-rectangular profiles, the next development that I'm trying to make). It can be flipped along its X, Y, or both X and Y directions, or restored to original.
-
Click again or release the mouse button after dragging to fix the rotation, orientation and flip state of the cross section.
-
Give a name to the component about to be created.
-
Drag the mouse, or click again, with inferencing, or type a length in the VCB, to determine the length of timber to draw and create it as a component with its origin at the originally picked corner, and the component axes aligned with its width, thickness and length.
-
For the drawn piece of timber, record its length and cross-section size and name in the ComponentInstance and its nominal or actual cross section in its ComponentDefinition.
-
Return the tool to its original state, ready to pick again and draw another piece, usually though not necessarily picking on a corner, edge, or face of any previously drawn piece of timber, or another object.
Ideally, you should be able to flip the cross section before or after the component is drawn, until the tool is next activated.
[NOTE: * I added this bit about the camera viewpoint in a revision of the post]
I hope that make the overall purpose clear.
I've been fairly clear all along WHAT I wanted to do. The bits I've had most trouble with are HOW to do that in code, since I'm still not very familiar with Ruby, in particular how it handles arrays.
I frequently get both the syntax and the semantics wrong - either can't get something to work at all, or it doesn't do what I expected it would, so I've experimented a lot, and usually get somewhere useful in the end. But I suspect that much of my code is horrible! I was trying both to simplify it, and learn about object-based programming in Ruby (which I understand in principle, and have done before in Visual Basic for Applications in MS Word, Access and to a small degree in Excel), and to a very limited extend in PHP.
But all have been self-taught, or learnt from books and code examples on the Internet, with help from forums like this one (or similar for other languages).
-
-
@johnwmcc said:
If you are storing Point3d objects, would you need
@contents.to_a
first, before.max.y
ormin.y
?The
min
andmax
methods come from theEnumerable
mixin module.
Enumerable
is mixed intoArray
, but notGeom::Point3d
, norGeom::Vector3d
.
To see what modules may be mixed into a class, use
*Classname*.ancestors
So to answer the question, yes, or if using your own collection class, you might mix in the
Enumerable
module into it.@johnwmcc said:
It does, for me. It was intended to select out the y coordinate (a scalar) from the point coordinates.
ary = [pt1, pt2, pt3, pt4, ... ]
To select the point which has the minimumy
value, from an array of points, do this:
miny_pt = ary.min {|a,b| a.y <=> b.y }
To select the point which has the maximumy
value, from an array of points, do this:
maxy_pt = ary.max {|a,b| a.y <=> b.y }
... then get your thickness:
thickness = maxy_pt.y - miny_pt.y
To translate: [ruby:247d5h9w]ary.max {|a,b| a.y <=> b.y }[/ruby:247d5h9w], into plain English:
"iterate [ruby:247d5h9w]ary[/ruby:247d5h9w], each loop compare the current member ([ruby:247d5h9w]a[/ruby:247d5h9w])'sy
value, against the next member ([ruby:247d5h9w]b[/ruby:247d5h9w])'sy
value, and return the member that resolves to the maximum." The comparison is defined using Ruby's comparison operator, [ruby:247d5h9w]<=>[/ruby:247d5h9w].
Remember that the entire member is returned, so later you still need to ask for just they
value.@johnwmcc said:
I wasn't sure if the [ruby:247d5h9w].x[/ruby:247d5h9w] and [ruby:247d5h9w].y[/ruby:247d5h9w] methods would work on arrays, but they seem to in my example code.
Because the SketchUp API adds those methods to the Ruby
Array
class. (It is part of making Ruby arrays compatible with API points and vectors.)@johnwmcc said:
I'm unclear about the near-but-not-complete equivalence between SU [ruby:247d5h9w]Point3d(x, y, z)[/ruby:247d5h9w] and [ruby:247d5h9w]Array[x, y, z][/ruby:247d5h9w]- it seems I can use either a lot of the time, but not quite always!
Read the API regarding how it adds to
Array
class:
http://www.sketchup.com/intl/en/developer/docs/ourdoc/array
Advertisement