You can get the angle of a TRACK with the angle command, then filter out values you don’t want, one at a time. (getting the angle again with “copy angle -90 int = not filter” for each distinct value of angle you want to remove). Once completely filtered, do a select refresh to select the tracks.
State of DRAWSEGMENTs (newdrawing command) and coordinate transformations (and a little about fonts)
geoms can be a list of strings and datatypes that specify geometries. As part of the development of newdrawing, it became apparent that a method to transform coordinates for subsequent geoms would be a useful construct. You could have a text file with geoms and read it in, and have it transformed anywhere and in any rotation/scale you want.
During the development of font handling, It became clear this is the good framework for handling the placement of character glyphs. An initial transform is retained that indicates the origin (and any other affine transformation)* of the current character. After the character glyph is rendered, the transformation is translated one step: the width of the just-rendered character. On a newline character (0x0D or 13 decimal), the transformation is moved “down” one line, based on the current transformation matrix. In this way, the character advance direction and “down” can be defined as any direction (e.g. as specified by the current transformation matrix), and characters can thus be rendered at any position, rotation, and scale and maintain their character spacing and line characteristics.
Thinking about this further, it would be difficult to separate changes to the character transformation matrix (moving to an absolute position on the board or moving to the next character position) and also within the character itself (to draw the character). In this scenario, “characters” could just as easily be other geoms. Further these could be nested arbitrarily deep, each with their own transformation requirements to maintain the relationship to other geoms at that level.
What this means to me is that we need a method to retain multiple transformations, go to a new one, then return to the old one. I would call this a “stack,” or more precisely, a “transformation matrix stack”. So if a self-contained geom needs to modify or specify a transformation, it can do so while affecting only subsequent geoms in its nesting level, but not those outside.
Let’s call these new geom specifiers as “TransformNew” and “TransformOld” where TransformNew pushes a new transformation on the stack, and TransformOld pops and discards the then-current transformation.
The entire flow would be something like:
- TransformNew [Other transform commands such as Translate, Scale, or Rotate] [geoms to be transformed] TransformOld
It looks to me like the units specification, which had been a precursor to the transformation commands, should instead only be relevant to transform commands themselves. To make a scale applicable to the current level of transformation commands, you would use the Scale command, perhaps with a units command within it. Something like:
- TransformNew,Scale,mm,1,1,Line,1,1,2,2,TransformOld - this would create a line from 1mm,1mm to 2mm,2mm. Which would slope from upper left to lower right.
It seems most appropriate that the default scale should be in native units and carry the same x/y direction of KiCAD. That is, positive x and positive y are right and down. Note this may be different than many graphics coordinate systems which might expect that to be right and up. To plot a graphic using coordinates in millimeters x/y positive in the right/up direction, you would specify
- TransformNew,Scale,mm,1,-1,Line,1,1,2,2,TransformOld - this would create a line from 1mm,1mm to 2mm,2mm. Which would slope from bottom left to upper right.
Finally, it might be useful to reset the scale back to normal and clear the stack, maybe with a command TransformReset or maybe TReset
The current transformation commands are Scale, Translate, Rotate, and Shear. Could there be a shorthand for these? Ts, Tt, Tr, Tz? Or even just S, T, R, Z? They could be extended to Sx, Sy, Tx, Ty, Zx, and Zy if you only want to specify x or y transform. To make this simpler, “T” looks like a sort of transform and could be a shorthand for each of these commands: T+, T-, and T0 (zero).
I’m still thinking about how to integrate geoms and the font capability as well as the best way to indicate transformations of geoms. I think this is a good start on that discussion.
* I like Wolfrom Mathworld’s description of Affine Transformation
Geometric contraction, expansion, dilation, reflection, rotation, shear, similarity transformations, spiral similarities, and translation are all affine transformations, as are their combinations. In general, an affine transformation is a composition of rotations, translations, dilations, and shears.
Hi @HiGreg ,
I noticed that the documentation string for “roundn” doesn’t mention the stack parameter N.
Is it possible to turn off the stack content status message when running user defined commands? For complicated commands the screen can fill up with the stack status messages as the command is executed. Here’s an example that creates unneeded stack messages:
: getxy “User ( TRACKS – TRACKS ) Print location information of first track segment in list” copy GetBoundingBox call GetCenter call 0 index copy 0 index 1 mm / list swap 1 index 1 mm / 2 int roundn concat 1 pick 0 index list angle 4 int roundn append 1 pick 0 index list getnetinfo getnetname append print pop ;
with the resulting output:
Would it be possible to add a command to move the display and center the cursor to a given X,Y location?
Agreed on both counts. I updated the development version for both issues and will test and it will be available on the next release. Both issues required small code changes to kicommand.py. Hopefully testing won’t reveal any additional required changes.
I’m in the middle of adding font support, so I want to at least make sure those changes don’t break anything currently, even if font support is not complete.
Thank you!
I’ll look into this, not sure if I can control the KiCAD GUI in a sustainable way. I’ll update this comment if I find something promising.
Edit: this looks promising, but I can’t find pcbnew.WindowZoom() in the current KiCAD version. I’m looking through KiCAD source code.
Edit2: I did find pcbnew.WindowZoom() in Python but it doesn’t seem to do anything on my version of KiCAD (5.1.6-release on Windows). I’ve defined this command:
- : windowzoom “Gui [x_y_w_h] input is a list of integers. Zoom to the box defined by x,y and w(idth), h(eight). This is probably useful when specifying parameters in mm or mils\nUsage should be similar to: 10,10,200,200 mm int windowzoom. mm,mil,mils,int” int pcbnew swap WindowZoom callargs ;
Edit3: I’ve abandoned this idea for now simply because KiCAD does not supply python commands that control the GUI. If you find out anything else promising, please let me know. If I run into anything promising in the future, I’ll update this thread.
Note that the description in the string (where you have ( TRACKS - TRACKS ) should actually be brackets and space-separated words only, within the argument definition. This is important in most definitions because KiCommand parses this string to determine the number of arguments to remove from the stack and send to the command. However, user commands don’t actually remove items from the stack themselves, items are removed during the execution of the actual commands within the UserCommand. On balance, this won’t affect user command, but in case it makes a difference in the future, the best practice is space-separated words within brackets.
Sorry for the way this sounds (perhaps a little alarming). This is a suggestion to be compatible in case this changes within KiCommand, but is not strictly necessary with UserCommands in the current version of KiCommand.
Thanks for that information! I was trying to be more consistent with documentation that would inform the state of the stack before and after the function call, similar to the granddaddy of stack languages Forth. I’ve built a small collection of stack operators based on those in Forth that are currently documented this way, but will go back and update them accordingly.
On another subject, I’ve been trying create a function to iterate the builtin “abs” operator over a list. This is what I have (working) now but is seems rather convoluted. Maybe you could suggest a simpler method.
: rot “User ( a b c – b c a ) Rotate third item to top of stack” 2 pull ;
: absl “User ( LIST – LIST ) Returns absolute values of list items” builtins map sindex list builtins abs sindex list rot append list fcallargs delist ;
I think it would also be useful if there was “map” functionality that operated with user defined functions.
I reviewed Forth for some of the basic ideas for KiCommand. It’s sort of a mix of Forth and Python. You can certainly add before/after stack documentation after the close bracket. Between the close bracket and the last word is arbitrary command explanation. The last word (separates by a space) is a comma separated list of commands printed out as the “SEE ALSO” section. You might consider instead of “User” as the category, something like “Forth”. The category can be a comma separated list o categories, and the specific categories are also relatively arbitrary. You can add your own or add commands to existing categories, and helpcat will show them in the indicates category. That said, the custom is that categories are single capital letter followed by lower case. The single capital letter ensures that it isn’t also a command itself (since the custom for command names is all lower case), so you’ll “never need” to escape the category with the single quote character when using helpcat.
I’ll look at your absl command to see if it can be simplified. One thing to note is that my hope is that fcallargs would work with both single items and list items. I may not have copied the argument handling from call/callargs yet, so i’ll look at that also. My hope is that you wouldn’t need all the list/delist shenanigans, but I know in many cases they are still needed to avoid syntax errors.
Another item is that it is also my custom to indicate that a specific command that works on a list ends with a period (see * and *. as an example). So in this case if you want to be consistent with other commands, you could define abs. for lists and abs for single numeric values. Then when you or I get it working for both single items and lists in one command, it would simply be abs without the period.
Regarding map, you’re thinking that should work like Python’s map() function?
Edit: sorry for all the typos, I’m on mobile at the moment.
I’m more of a Ruby programmer, but yes I think that adding the functional language ‘trinity’ of map, reduce, select (or filter)] would certainly increase the power and flexibility of KiCommand.
It would seem that the complexity of having to parse intent from syntax of an objects iterator functionality could be simplified with explicit calls to map/reduce/select.
Thanks for the input, I’ll see if I can do anything about that. I need to review whether special handling will be needed for specifying a user command as an argument (it would certainly include escaping it with a single quote).
On your implementation of absl it seems it would be greatly simplified if fcallargs simply repeated the shorter list to match the length of the longer one. I can’t recall the specific use case of current implementation of truncating the argument list to match the length of the function list. If I’m able to change fcallargs in this way, then the absl command becomes (float added so it works with a comma-separated “string” of floats)
- float list. builtins 'abs sindex list swap fcallargs
I’m also not a fan of the current requirement of builtins 'abs sindex but it seems short enough to not have to create a function that would be used like 'abs builtin instead of builtins 'abs sindex
Edit!! If you want this functionality for fcallargs, it’ll be in the next release. In the meantime you can edit the definition of fcallargs in kicommand.py. Make the following change (line 4821 in the current KiCommand release):
From
zip(c[0], cycle(c[1]))
To
itertools.islice( itertools.izip( itertools.cycle(c[0]), itertools.cycle(c[1])), max(len(c[0]),len(c[1])))
This should work in Python2. Not sure about Python3.
I’m still thinking about how to implement this, there are very few commands that operate using KiCommand commands as an “argument.” Preceding a command with ? is one (kind of) that executes a command conditionally. If any command starts with ?, then the command is executed only of the top of the stack is True.
I’ve also been thinking about the viability and use of a loop command, where a longer command is executed repeatedly as a conditional until the top of the stack is false. If implemented such that the loop occurs over a single command, then parsing is easier without requiring an endloop (or similar). In this case, the body of the loop could be defined as a user command prior to the loop. In the most common use case, the command being looped would probably end with copy_or_pick function_that_produces_a_bool. Thinking out loud: seems like preceding the command being looped with “??” might be a good choice since “?” is already being recognized as special (and therefore “??” wouldn’t increase parsing speed).
Still thinking through map/reduce/filter commands. I think filter function can be accomplished with a map command that executes a KiCommand command on each member. Once complete and a True/False value is obtained, the current KiCommand filter command can filter the original list of objects with the True/False list created subsequently (possibly from a new map command).
I’m having difficulty thinking of a use case for reduce. Total length of DRAWSEGMENTs is one use case, but that can be accomplished straightforwardly with current commands.
The KiCommand version that includes preliminary font support has been published to github. You should plan to struggle with figuring it out. But here are the basic steps. Fonts can only be placed in the install directory. I know this is not optimal, but I plan to add search in user directory.
- Install fonttools in your system version of Python3. This should be installable with “pip install fonttools” into your main Python3 installation (or suitable virtual environment).
- Use fontimport.py command with Python3 with the installed fonttools. Use something similar to “python fontimport.py” on your command line to get help with the command.
- Output font files into location of kicommand plugin directory under “fontdata” (which you must manually create).
- The font name is pulled from the font file with dashes replacing spaces and first letter capitalized: something like “Noto-sans-regular”.
- Go into KiCommand and execute the following command:
- Noto-sans-regular setfont
- Now enter your text similar to the following command:
- "Here is the text on the board!" stringtogeom newdrawing refresh
- You should now see the text with the lower left baseline at 0, 0 and the size of text approximately 10/72" (3.5mm) tall.
- Use scaling and/or translation commands to resize and move the text.
- S,4,Tmm,200,150 split “Text just right” stringtogeom append newdrawing refresh
- Use rotation commands to put the text on an angle:
- S,4,R,45,Tmm,200,150 split “Rotated text” stringtogeom append newdrawing refresh
Edit: I forgot to include the brief update text
Initial font support, see “Font helpcat” and the independent Python script fontimport.py. Params is now a stack. Printing number of stack elements deferred to end of command. wxPointUtil no longer creates wxPoints, uses tuples instead. Added pprint (pretty print), toggleselect, getsymbolgeom. Added 2d transformations within geoms. Updated fcallargs to repeat shortest list. Added geoms “Polygon.” and “Hole” which extend a previous “Polygon”. New transformation commands in a geom: “S,Sx,Sy,R,Z,+T,T,Tmm,Tmil,Tmils,T+,T-” and removed mm, mil, and mils.
Has anyone had a chance to test the fonts? Anyone need any help?
Looks like something in the new version broke the ‘grid’ command.
It not longer snaps the end X,Y points of a trace segment.
Maybe you can add a command to print a release version # so it makes it easy to make sure what code release is being referred to.
Also looking through some of the code release comments, it implies that there is a fix for the excessive stack messages. Do I need to set a variable to enable this? I’m still getting quite a trail of stack messages when running my user commands.
putting tracks on grid is working for me. You should have the following on line 923 of kicommand.py:
if isinstance(seg, (pcbnew.DRAWSEGMENT, pcbnew.TRACK) ):
I’ll see if I can add a version number for the next release.
The printing of “5 operands left on the stack.” is also only at the end of execution, not during/in the middle. There is no variable to set, it should be happening for every command.
Line 580 of kicommand.py should read:
if len(_stack) and not suspendStackNumPrint:
Try the command getbottomrowcounts you can see with 'getbottomrowcounts see that it is made up of many other commands. It should only print “X operands left on the stack.” one time.
The version I installed didn’t match the content on the given line numbers. I downloaded and re-installed from the zip, and it all works again. Now only prints stack info once after my user commands.
Thanks for your help!
I’ve spent sometime trying to understand the non-45 modulus issue after a snap to grid and I think I’ve found the cause. It looks like there is a problem in the ‘grid’ command with int() truncation causing points to move to the wrong grid value. Here’s the original code:
def GRID(dseglist,grid):
grid = int(grid)
for seg in dseglist:
if isinstance(seg, (pcbnew.DRAWSEGMENT, pcbnew.TRACK) ):
for gp,sp in ((seg.GetStart,seg.SetStart),(seg.GetEnd,seg.SetEnd)):
p = gp()
newp=pcbnew.wxPoint(int(round(p[0]/grid)*grid),int(round(p[1]/grid)*grid))
sp(newp)
else: # this works for BOARD_ITEMs
for gp,sp in ((seg.GetPosition,seg.SetPosition),):
p = gp()
newp=pcbnew.wxPoint(int(round(p[0]/grid)*grid),int(round(p[1]/grid)*grid))
sp(newp)
and here’s a test mod that fixes the problem by removing initial casting of grid to int and doing an additional round() during calculation of newp
def GRIDX(dseglist,grid):
# grid = int(grid)
for seg in dseglist:
if isinstance(seg, (pcbnew.DRAWSEGMENT, pcbnew.TRACK) ):
for gp,sp in ((seg.GetStart,seg.SetStart),(seg.GetEnd,seg.SetEnd)):
p = gp()
newp=pcbnew.wxPoint(int(round(round(p[0]/grid)*grid)),int(round(round(p[1]/grid)*grid)))
sp(newp)
else: # this works for BOARD_ITEMs
for gp,sp in ((seg.GetPosition,seg.SetPosition),):
p = gp()
newp=pcbnew.wxPoint(int(round(round(p[0]/grid)*grid)),int(round(round(p[1]/grid)*grid)))
sp(newp)w
Wow. This is a great find. I really appreciate your effort in tracking this down! In your use case, the track starts out on some grid and starts out as a 45-degree segment? I’ll make this change in the source so it will be a part of the next release.
Seems that moving track end points off grid is a “feature” of the push/shove router. The router normally respects the grid, but when moving groups of tracks that are bumping up against design rules it appears to prioritize maintaining 45 degree angles and will move track end points off grid to achieve this.