initially adding the graph and presentation ipelets
[Misc/ipe.git] / ipelets / graph / graph.lua
1 ----------------------------------------------------------------------
2 -- graph ipelet
3 ----------------------------------------------------------------------
4 label = "Graph"
5
6 about = [[ Some features making it easier to work with graphs. ]]
7
8 local deactivateGraphMode = false
9
10 function toggleGraphMode ()
11    if deactivateGraphMode then
12       deactivateGraphMode = false
13    else 
14       deactivateGraphMode = true
15    end
16 end
17
18 local editing = false
19 local currMarkId = nil
20
21 --------------------------------------------------------------------------------
22 -- add an edit action for marks ------------------------------------------------
23
24 -- saving the old function
25 function _G.MODEL:graph_backup_actinon_edit () end
26 _G.MODEL.graph_backup_action_edit = _G.MODEL.action_edit
27
28 -- adding support for objects of type reference
29 function _G.MODEL:action_edit()
30    if deactivateGraphMode then
31       self:graph_backup_action_edit()
32       return
33    end
34    local p = self:page()
35    local prim = p:primarySelection()
36    if not prim then
37       self:graph_backup_action_edit()
38       return 
39    end
40    local obj = p[prim]
41    if obj:type() == "reference" then
42       action_edit_reference (self, prim, obj)     
43    else
44       self:graph_backup_action_edit()
45    end
46 end
47
48
49 -- starting to edit a mark
50 function action_edit_reference(model, prim, obj)
51    editing = true
52    currMarkId = prim
53    local p = model:page()
54
55    local pos = obj:matrix() * obj:position()
56    -- print(pos)
57
58    -- creating a circle at the position of the mark
59    local ellipse = {type="ellipse"}
60    ellipse[1] = ipe.Matrix({10, 0, 0, 10, pos.x, pos.y})
61    -- print(ellipse[1])
62    local circ = ipe.Path(model.attributes, {ellipse})
63    p:insert(nil, circ, 0, p:layerOf(prim))
64
65    -- edit the circle instead of the mark itself
66    model:action_edit_path(#p, circ)
67
68    -- print("test")
69 end
70
71 --------------------------------------------------------------------------------
72 -- pressing a key while editing the cycle --------------------------------------
73
74 -- saving old function
75 function _G.EDITTOOL:graph_backup_key(code, modifiers, text) end
76 _G.EDITTOOL.graph_backup_key = _G.EDITTOOL.key
77
78 -- overwriting
79 function _G.EDITTOOL:key(code, modifiers, text)
80    self:graph_backup_key(code, modifiers, text)
81    if deactivateGraphMode then return end
82
83    -- react if and only if we are currently editing a mark and key ESC
84    -- or SPACE is pressed
85    if text ~= "\027" and code ~= 0x20 then return end
86    if not editing then return end
87
88    editing = false
89
90    -- finding new and old position
91    local p = self.model:page()
92    local circ = p[#p]
93    local mark = p[currMarkId]
94    local oldPos = mark:matrix() * mark:position()
95    local newPos = circ:shape()[1][1]:translation()
96
97    -- remove the intermediate step of moving the cycle from the undo
98    -- stack and remove the cycle itself
99    local undoSt = self.model.undo
100    p:remove(#p)
101    table.remove(undoSt)
102
103    -- new action for the undo stack moving the mark and all endpoints
104    -- ending at the mark
105    local t = { label = "edit reference",
106                pno = self.model.pno,
107                vno = self.model.vno,
108                selection = self.model:selection(),
109                original = self.model:page():clone(),
110                matrix = matrix,
111                undo = _G.revertOriginal,}
112    t.redo = function (t, doc)
113       p:transform(currMarkId, ipe.Translation(newPos-oldPos))
114       moveEndpoints(oldPos, newPos, p)
115    end
116    self.model:register(t)
117 end
118
119 -- function moving all endpoints and intermediate points in polylines
120 -- to newPos, if the squared distance to oldPos is at most sqEps
121 local sqEps = 1
122 function moveEndpoints(oldPos, newPos, p)
123    for i, obj, sel, layer in p:objects() do
124       if obj:type() == "path" then
125          local shape = obj:shape()
126          for _, subPath in ipairs(shape) do
127             if (subPath["type"] == "curve") then
128                for _,seg in ipairs(subPath) do
129                   if (seg["type"] == "segment") then
130                      for j, point in ipairs(seg) do
131                         -- print(j, point, oldPos)
132                         if (obj:matrix() * point - oldPos):sqLen() < sqEps then
133                            seg[j] = obj:matrix():inverse() * newPos
134                            -- print("test", seg[j])
135                         end 
136                      end
137                   elseif (seg["type"] == "spline") then
138                      if (obj:matrix() * seg[1] - oldPos):sqLen() < sqEps then
139                         seg[1] = obj:matrix():inverse() * newPos
140                      end
141                      if (obj:matrix() * seg[#seg] - oldPos):sqLen() < sqEps then
142                         seg[#seg] = obj:matrix():inverse() * newPos
143                      end
144                   end
145                end
146             end
147             obj:setShape(shape)
148          end
149       end
150    end
151 end
152
153
154 --------------------------------------------------------------------------------
155 -- working with groups ---------------------------------------------------------
156
157 local function regroup(elem)
158    local groupElem = {}
159    for i, obj in ipairs(elem) do
160       if obj[1] ~= nil then
161          groupElem[#groupElem + 1] =  regroup(obj)
162       else 
163          groupElem[#groupElem + 1] = obj
164       end
165    end
166    return ipe.Group(groupElem)
167 end
168
169 local function ungroup(group)
170    local elem = group:elements()
171    local plainElem = {}
172    for i, obj in ipairs(elem) do
173       if (obj:type() == "group") then
174          local subElem, subPlainElem = ungroup(obj)
175          elem[i] = subElem;
176          for _, subObj in ipairs(subPlainElem) do
177             table.insert(plainElem, subObj)
178          end
179       else
180          table.insert(plainElem, obj)
181       end
182    end
183    return elem, plainElem
184 end
185
186 --------------------------------------------------------------------------------
187 -- shorten paths ---------------------------------------------------------------
188
189 function shortenObj(obj, lenSource, lenTarget)
190    if obj:type() == "path" then
191       local shape = obj:shape()
192       for _, subPath in ipairs(shape) do
193          local first = subPath[1]
194          local last = subPath[#subPath]
195          
196          local p1 = obj:matrix() * first[1]
197          local p2 = obj:matrix() * first[2]
198          local pDelta = p2 - p1
199          local pNorm = pDelta:normalized()
200          local newP1 =  p1 + pNorm*lenSource
201          
202          local q1 = obj:matrix() * last[#last]
203          local q2 = obj:matrix() * last[#last - 1]
204          local qDelta = q2 - q1
205          local qNorm = qDelta:normalized()
206          local newQ1 = q1 + qNorm*lenTarget
207
208          first[1] = obj:matrix():inverse() * newP1
209          last[#last] = obj:matrix():inverse() * newQ1
210       end
211       obj:setShape(shape)
212    end
213 end
214
215 function getString(model, string)
216    if ipeui.getString ~= nil then
217       return ipeui.getString(model.ui, "Enter length")
218    else 
219       return model:getString("Enter length")
220    end
221 end
222
223 function shorten(model, num)
224    num = num - 1
225    local lenTarget = 0
226    local lenSource = 0
227    -- local str = ipeui.getString(model.ui, "Enter length")
228    -- local str = model:getString("Enter length")
229    local str = getString(model, "Enter length")
230    if not str or str:match("^%s*$)") then return end
231    if num == 1 then -- shorten target
232       lenTarget = tonumber(str)
233    elseif num == 2 then -- shorten source
234       lenSource = tonumber(str)
235    elseif num == 3 then -- shorten both
236       lenTarget = tonumber(str)
237       lenSource = tonumber(str)
238    end
239
240    -- start to edit the edges
241    local t = { label = "shorten edges",
242                pno = model.pno,
243                vno = model.vno,
244                selection = model:selection(),
245                original = model:page():clone(),
246                matrix = matrix,
247                undo = _G.revertOriginal,}
248    t.redo = function (t, doc)
249       local p = doc[t.pno]
250       for _, i in ipairs(t.selection) do
251          p:setSelect(i, 2)
252       end
253       local p = doc[t.pno]
254       for i, obj, sel, layer in p:objects() do
255          if sel and obj:type() == "group" then
256             local elem, plainElem = ungroup(obj)
257             for _,subobj in pairs(plainElem) do
258                shortenObj(subobj, lenSource, lenTarget)
259             end
260             p:replace(i, regroup(elem))
261          elseif sel then
262             shortenObj(obj, lenSource, lenTarget)
263          end        
264       end
265    end
266    model:register(t)
267 end
268
269
270 methods = {
271    { label = "toggle graph mode", run=toggleGraphMode },
272    { label = "shorten target", run=shorten },
273    { label = "shorten source", run=shorten },
274    { label = "shorten both", run=shorten },
275 }
276
277 ----------------------------------------------------------------------