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