Origin of shapes in Applescript

I am writing a script that sets chairs evenly around a circular table.
I used the “And Gate” shape for this but the origin appears to be at top left of shape, but not for all objects. I am setting the chairs in a circle, so would prefer to have the origin on the shape centreline so the maths is not diabolical.

PS this image I have manually located the chairs. The resultant applescript I am finding it hard to generalise the algorithm

Sketching it in JavaScript for Automation (which, unlike AppleScript, has Math.sin and Math.cos functions in the standard library), you could position the absolute centre simply by deducting half the shape width from the x position and half the shape height from the y position.

This would allow you to generate something like this:

from code of this kind:

(function () {
    'use strict';

    // positionAbsByCentre :: Shape -> (Real, Real) -> IO ()
    //        -> (Real, Real)
    function positionAbsCentre(shp, lstCentreXY) {
        var lstSize = shp.size(),
            lstOrigin = [
                lstCentreXY[0] - lstSize[0] / 2,
                lstCentreXY[1] - lstSize[1] / 2
            ];

        return (
            shp.origin = lstOrigin,
            lstOrigin
        );
    }

    // positionRelRadialCentre :: Shape -> (Real, Real) -> Real -> Real
    //        -> IO() -> (Real, Real)
    function positionRelRadialCentre(shp, lstOriginXY, rDegrees, rDistance) {
        var radians = (Math.PI / 180) * rDegrees;

        // Absolute position by centre
        return positionAbsCentre(
            shp, [
                lstOriginXY[0] + Math.sin(radians) * rDistance,
                lstOriginXY[1] - Math.cos(radians) * rDistance
            ]
        );
    }

    // radiallyPlacedAndRotated :: Shape -> Int -> Int -> Real ->
    //        (Real, Real) -> Real -> Real -> IO () -> (Real, Real)
    function radiallyPlacedAndRotated(
        shp, total, index, rStartRotn, lstOrigin, rDistance
    ) {
        var rDegrees = index * (360 / total);

        return (
            shp.rotation = rStartRotn + rDegrees,
            positionRelRadialCentre(shp, lstOrigin, rDegrees, rDistance)
        );
    }

    // N copies of a named shape at given size
    function circleOfShapes(oCanvas, intShapes, strShapeName, lstOrigin,
        rDistance, lstSize, rStartRotn) {

        var graphics = oCanvas.graphics;

        return range(0, intShapes - 1)
            .map(function () {
                var oShape = og.Shape({
                    name: strShapeName,
                    size: lstSize
                });

                return (
                    graphics.push(oShape),
                    oShape
                );
            })
            .map(function (shp, i) {
                return radiallyPlacedAndRotated(
                    shp, intShapes, i,
                    rStartRotn, lstOrigin, rDistance
                );
            });
    }

    // [m..n]
    // Int -> Int -> [Int]
    function range(m, n) {
        return Array.apply(null, Array(n - m + 1))
            .map(function (x, i) {
                return m + i;
            });
    }

    var og = Application('OmniGraffle'),

        ws = og.windows,
        w = ws.length ? ws[0] : undefined;

    return circleOfShapes(
        w.canvas(), // canvas of front window
        8, // number of shapes
        "AndGate", // name of shape
        [250, 250], // centre point [X, Y]
        120, // distance from centre point to shape centres
        [50, 50], // Width, Height of shapes
        -90 // rotation of top shape
    );

})();

Hi

thanks for this feedback; I’m restricted to Applescript as I call it from Filemaker Pro.

However I’m trying to see if I can use your approach to use the centroid/centreline, so thanks for that tip.
I’ll post a solution if I get it to work

Being unused to javascript I have some difficulty understanding you code but I will extract what I can.

if FM applescript actions allow you to use ‘do shell script’ you could run a js script as an osascript command line

(nested string quotation might prove burdensome)

I have made some progress and now can draw this


The deep blue chair is where it should be located, but I cannot get the property origin of the shape or its ID to move it to its proper location.

Can anyone give me an example code:
both of these statements fail

tell application id “com.omnigroup.OmniGraffle6”
tell canvas of front window
set shapeRef to (make new shape with properties {name:“AndGate”, textSize:{1.0, 0.8}, draws shadow:false, size:{42.0, 42.0}, text:{alignment:center, font:perFont, text:perINIT}, textRotation:90, rotation:tAngle, origin:{xPos, yPos}, thickness:0, textPosition:{0.0, 0.1}, vertical padding:0, fill color:{0.0, 1.0, 1.0, 0.304984}})
#set shapeID to (origin of properties of shapeRef)
#set itemOneProps to properties of shape 1
end tell
end tell

It would be helpful to see a little more of your code.

You need to assign a list of two numbers to the shape.origin property

something like:

  set someShape.origin to {50, 100}

Where are you deriving the values of that {rX, rY} pair ?

Hi

I have made further improvements…

which is easily moved manually to its correct location, but Applescript should be able to do it.
The line outer endpoints appear to exactly (but may be a wrong interpretation) coincide with the centroid of the chair.

I’m happy to share the code:
property perFont : “GillSans”
property Seat : 42
property Gap : 3
property seatSpace : Seat + Gap

set memberCount to 15
set tPerim to memberCount * (seatSpace)
set tRadius to tPerim / (2 * pi)
if tRadius < 40 then set tRadius to 60

set attList to {“GBa”, “JMi”, “PMi”, “DMa”, “SMi”, “PDo”, “RVi”, “JMi”, “PMi”, “DMa”, “SMi”, “PDo”, “RVi”, “JMi”, “PMi”, “DMa”, “SMi”, “PDo”, “RVi”, “JMi”, “PMi”, “DMa”, “SMi”, “PDo”, “RVi”}

tell application id “com.omnigroup.OmniGraffle6”
tell canvas of front window

	# draw circular table
	set xOrigin to 150
	set yOrigin to 250
	set tDiam to 2 * tRadius
	set pointList to {0, xOrigin, yOrigin}
	make new layer at end with properties {name:"Table"}
	--	select layer id 2
	make new shape with properties {name:"Circle", textSize:{0.8, 0.7}, draws shadow:false, size:{tDiam, tDiam}, origin:{xOrigin, yOrigin}, textPosition:{0.1, 0.15}}
	
	# now draw seats
	# - set coords of centre of table
	--set tRadius to tRadius / 2
	set xtCentre to xOrigin + tRadius
	set ytCentre to yOrigin + tRadius
	
	make new layer at end with properties {name:"Seats"}
	--select layer 1 ??
	
	repeat with i from 1 to memberCount
		#functions are in radians
		set tAngle to (i * (360 / memberCount))
		set tRadians to tAngle * (2 * pi) / 360
		# set chair offsets from centre
		set Hoff to (tRadius + (Seat / 2) + Gap) * (cos (tRadians))
		set Voff to (tRadius + (Seat / 2) + Gap) * (sin (tRadians))
		#determine coords of seat 
		set xPos to xtCentre + Hoff
		set yPos to ytCentre + Voff
		# get member initials
		set perINIT to text item i of attList
		
		# draw a line from table centre to Chair origin
		make new line with properties {point list:{{xtCentre, ytCentre}, {xPos, yPos}}, thickness:0.25}
		
		set shapeRef to (make new shape with properties {name:"AndGate", textSize:{1.0, 0.8}, draws shadow:false, size:{42.0, 42.0}, text:{alignment:center, font:perFont, text:perINIT}, textRotation:90, rotation:tAngle, origin:{xPos, yPos}, thickness:0, textPosition:{0.0, 0.1}, vertical padding:0, fill color:{0.0, 1.0, 1.0, 0.304984}})
	end repeat
	
	set theOffset to {-20, -20}
	--set currentLayer to layer with name:"Seating"
	--		set allGraphics to shapes of currentLayer
	--		slide allGraphics by theOffset
end tell

end tell

The OG interface’s origin property is at the top left of a shape, so you just need to give it coordinates half a seat-size to the left of the calculated centroid, and half a seat-size above it.

#determine coords of seat 
set xCentre to xtCentre + Hoff
set yCentre to ytCentre + Voff

-- Delta
set xPos to xCentre - halfSeat
set yPos to yCentre - halfSeat

I think you may be pulling in sin and cos functions from an osax library (perhaps Satimage ?) so to make the following work I tacked on a slow import of Math.sin and Math.cos from JavaScript

(from evalOSA onwards, plus the use Framework line at the top of the script)

and this also meant that I had to add the keyword “my” before the sin and cos calls, but this should be testable and give you a sense of the positioning edits I made.

(For FileMaker use, you may need to revert to your existing sin and cos approach, and remove the evalOSA version)

Note: I wasn’t sure at first what your variable Seat was, so in this copy I’ve renamed it seatSize and used it in the make new shape expression as well.

use framework "Foundation"

property perFont : "GillSans"
property seatSize : 42
property Gap : 3
property seatSpace : seatSize + Gap

on run
    set memberCount to 15
    set tPerim to memberCount * (seatSpace)
    set tRadius to tPerim / (2 * pi)
    if tRadius < 40 then set tRadius to 60
    
    set attList to {"GBa", "JMi", "PMi", "DMa", "SMi", "PDo", "RVi", "JMi", "PMi", "DMa", "SMi", "PDo", "RVi", "JMi", "PMi", "DMa", "SMi", "PDo", "RVi", "JMi", "PMi", "DMa", "SMi", "PDo", "RVi"}
    
    tell application id "com.omnigroup.OmniGraffle6"
        tell canvas of front window
            
            # draw circular table
            set xOrigin to 150
            set yOrigin to 250
            set tDiam to 2 * tRadius
            set pointList to {0, xOrigin, yOrigin}
            make new layer at end with properties {name:"Table"}
            --    select layer id 2
            make new shape with properties {name:"Circle", textSize:{0.8, 0.7}, draws shadow:false, size:{tDiam, tDiam}, origin:{xOrigin, yOrigin}, textPosition:{0.1, 0.15}}
            
            # now draw seats
            # - set coords of centre of table
            --set tRadius to tRadius / 2
            set xtCentre to xOrigin + tRadius
            set ytCentre to yOrigin + tRadius
            
            set halfSeat to seatSize / 2
            
            make new layer at end with properties {name:"Seats"}
            --select layer 1 ??
            
            repeat with i from 1 to memberCount
                #functions are in radians
                set tAngle to (i * (360 / memberCount))
                set tRadians to tAngle * (2 * pi) / 360
                # set chair offsets from centre
                set Hoff to (tRadius + halfSeat + Gap) * (my cos(tRadians))
                set Voff to (tRadius + halfSeat + Gap) * (my sin(tRadians))
                #determine coords of seat 
                set xCentre to xtCentre + Hoff
                set yCentre to ytCentre + Voff
                
                -- Delta
                set xPos to xCentre - halfSeat
                set yPos to yCentre - halfSeat
                
                # get member initials
                set perINIT to text item i of attList
                
                # draw a line from table centre to Chair origin
                make new line with properties {point list:{{xtCentre, ytCentre}, {xCentre, yCentre}}, thickness:0.25}
                
                set shapeRef to (make new shape with properties {name:"AndGate", textSize:{1.0, 0.8}, draws shadow:false, size:{seatSize, seatSize}, text:{alignment:center, font:perFont, text:perINIT}, textRotation:90, rotation:tAngle, origin:{xPos, yPos}, thickness:0, textPosition:{0.0, 0.1}, vertical padding:0, fill color:{0.0, 1.0, 1.0, 0.304984}})
            end repeat
            
        end tell
    end tell
end run

-- evalOSA :: ("JavaScript" | "AppleScript") -> String -> String
on evalOSA(strLang, strCode)
    
    set ca to current application
    set oScript to ca's OSAScript's alloc's initWithSource:strCode ¬
        |language|:(ca's OSALanguage's languageForName:(strLang))
    
    set {blnCompiled, oError} to oScript's compileAndReturnError:(reference)
    
    if blnCompiled then
        set {oDesc, oError} to oScript's executeAndReturnError:(reference)
        if (oError is missing value) then return oDesc's stringValue as text
    end if
    
    return oError's NSLocalizedDescription as text
end evalOSA

on cos(x)
    evalOSA("JavaScript", "Math.cos(" & (x as string) & ")") as real
end cos

on sin(x)
    evalOSA("JavaScript", "Math.sin(" & (x as string) & ")") as real
end sin
1 Like

Hi draft8

tks for this final elucidation. When I first saw you code I didn’t think it could work as your last delta was using orthogonal coordinates, rather than rotated coords. Shapes I’ve learnt rotate about their centroids but are set with an origin at top left. So in the example below - which now geometrically is behaving beautifully - I added the delta rectangle just ot prove to myself it works (which it did!).


I am still searching for an OmniGraffle Applescript primer which I’ve requested support who are helpful but…

  • how do you select an object
  • how do you select a layer for editing
  • how do you modify a property of an object

etc

thanks for all your assistance - most appreciated

1 Like

how do you select an object

The selection property of the front window is a settable (as well as readable) list,
so you can assign a fresh list to it

set oWindow.selection to {oObject}

or add new objects to the existing list

set lstSeln to selection of oWindow
set end of lstSeln to oExtraShape
set selection of oWindow to lstSeln

how do you select a layer for editing

For editing by script or by hand ?
(by script, you can identify a particular layer by name or index,
and then read/write properties of the graphics collection of that layer)

To set the scene for manual editing of a particular layer, you can make it the only one
that is unlocked (and even the only one that is visible)

tell application "OmniGraffle"
    tell canvas of front window
        
        set lstLayers to (layers of it) as list
        
        repeat with oLayer in lstLayers
            set locked of oLayer to true
            
            -- possibly invisible too
            -- set visible of oLayer to false  
        end repeat
        
        
        -- Make your chosen layer the only one that is unlocked
        -- (additional shapes will now be placed on it by default)

        set oLayer to item 3 of lstLayers
        -- or by name:
        -- set oLayer to layer "Seats"

        set visible of oLayer to true
        set locked of oLayer to false
    end tell
end tell

how do you modify a property of an object

set [propertyName] of oObject to varValue

1 Like