fancy decorator
[Misc/ipe.git] / ipelets / decorator / decorator.lua
1
2 -- return a table of names associated with decorator symbols
3 function decorator_names(model)
4    local sheets = model.doc:sheets()
5    local symbols = sheets:allNames("symbol")
6    local res = {}
7    for _, name in pairs(symbols) do
8       if name:find("deco/") == 1 then
9          res[#res + 1] = name
10       end
11    end
12    return res
13 end
14
15 -- Changes the transformation matrix of a path object to the identitiy
16 -- matrix without canging the appearance of the object (i.e., the
17 -- transformation matrix is applied to the objects shape)
18 function cleanup_matrix(path_obj)
19    local matrix = path_obj:matrix()
20    local matrix_func = function (point) return matrix end
21    local shape = path_obj:shape()
22    transform_shape(shape, matrix_func)
23    path_obj:setShape(shape)
24    path_obj:setMatrix(ipe.Matrix())
25 end
26
27 -- Transform a shape by transforming every point using a matrix
28 -- returend by matrix_func.  The function matrix_func should take a
29 -- point and return a matrix (that is then used to transform this
30 -- point).
31 --
32 -- Arcs are also transformed.
33 function transform_shape(shape, matrix_func)
34    for _,path in pairs(shape) do
35       for _,subpath in ipairs(path) do
36          -- apply to every point
37          for i,point in ipairs(subpath) do
38             subpath[i] = matrix_func(point) * point
39          end
40
41          -- apply to arcs
42          if (subpath["type"] == "arc") then
43             local arc = subpath["arc"]
44             local center = arc:matrix():translation()
45             subpath["arc"] = matrix_func(center) * arc
46          end
47       end
48    end
49 end
50
51 -- Resizes a given shape such that the same transformation also
52 -- transforms bbox_source to bbox_target.  The transformation is done
53 -- by translating points of the shape such that all points in the same
54 -- quadrant with respect to center are translated in the same way.
55 --
56 -- Clearly, the center has to lie inside bbox_source.
57 function resize_shape(shape, center, bbox_source, bbox_target)
58    -- assert that the center lies inside bbox_source
59    assert(bbox_source:left() < center.x)
60    assert(center.x < bbox_source:left() + bbox_source:width())
61    assert(bbox_source:bottom() < center.y)
62    assert(center.y < bbox_source:bottom() + bbox_source:height())
63
64    -- translation of points to the left/right/top/bottom of the center
65    local dx_left = bbox_target:left() - bbox_source:left()
66    local dy_bottom = bbox_target:bottom() - bbox_source:bottom()
67    local dx_right = bbox_target:width() - bbox_source:width()
68    local dy_top = bbox_target:height() - bbox_source:height()
69
70    -- transformation
71    local matrix_func = function (point)
72       local dx = dx_left
73       local dy = dy_bottom
74       if (center.x < point.x) then dx = dx + dx_right end
75       if (center.y < point.y) then dy = dy + dy_top end
76       return ipe.Translation(dx, dy)
77    end
78    transform_shape(shape, matrix_func)
79 end
80
81 function bbox(obj, page)
82    local objno = #page + 1
83    page:insert(objno, obj, nil, page:layers()[1])
84    local bbox = page:bbox(objno)
85    page:remove(objno)
86    return bbox
87 end
88
89 function report_problem(model, text)
90    ipeui.messageBox(mainWindow(model), "warning", text, nil, nil)
91 end
92
93 function run_fancy_decorator (model)
94    local p = model:page()
95    local prim = p:primarySelection()
96    local bbox_target = p:bbox(prim)
97
98    local deco_obj_group = ask_for_decorator(model)
99    if (deco_obj_group:type() ~= "group") then
100       report_problem(model, "The decoration must be a group.")
101       return
102    end
103
104    local objects = deco_obj_group:elements()
105    local last_obj = table.remove(objects, #objects)
106    local bbox_source = bbox(last_obj, p)
107    local center = ipe.Vector(bbox_source:left() + 0.5 * bbox_source:width(),
108                              bbox_source:bottom() + 0.5 * bbox_source:height())
109    
110    for i,deco_obj in ipairs(objects) do
111       if (deco_obj:type() ~= "path") then
112          report_problem(model, "Each decoration object needs to be a path.")
113          return
114       end
115
116       cleanup_matrix(deco_obj)
117       local deco_shape = deco_obj:shape()
118       
119       resize_shape(deco_shape, center, bbox_source, bbox_target)
120
121       deco_obj:setShape(deco_shape)
122    end
123
124    local group = ipe.Group(objects)
125
126    model:creation("fancy decoration created", group)
127 end
128
129 -- Asks the user for a decorator and returns the chosen decorator
130 -- object or nil.
131 function ask_for_decorator(model)
132    local dialog = ipeui.Dialog(mainWindow(model), "Select a decorator.")
133    local decorators = decorator_names(model)
134    dialog:add("deco", "combo", decorators, 1, 1, 1, 2)
135    dialog:add("ok", "button", { label="&Ok", action="accept" }, 2, 2)
136    dialog:add("cancel", "button", { label="&Cancel", action="reject" }, 2, 1)
137    local r = dialog:execute()
138    if not r then return end
139    local deco_name = decorators[dialog:get("deco")]
140    local symbol = model.doc:sheets():find("symbol", deco_name)
141    return symbol:clone()
142 end
143
144 -- Decorate something given by its bounding box with a given deco
145 -- object, which needs to be a path.
146 function decorate(model, bbox, deco)
147    if (deco:type() ~= "path") then
148       report_problem(model, "The decoration needs to be a path.")
149       return
150    end
151
152    local shape = deco:shape()
153    local m = deco:matrix()
154    for _,path in pairs(shape) do
155       for _,subpath in ipairs(path) do   
156          -- move all points
157          for i,point in ipairs(subpath) do
158             subpath[i] = translation(bbox, m*point) * m*point
159          end
160
161          -- for acs, the center must be translated separately
162          if (subpath["type"] == "arc") then
163             local arc = subpath["arc"]
164             local arc_pos = arc:matrix():translation()
165             subpath["arc"] = translation(bbox, m*arc_pos) * m * arc
166          end
167       end
168    end
169    -- update model
170    deco:setShape(shape)
171    deco:setMatrix(ipe.Matrix())
172    model:creation("create", deco)
173 end
174
175 -- The translation matrix that should be applied to a given point when
176 -- doing the decoration.
177 function translation(bbox, point)
178    local dx = 0
179    local dy = 0
180    if (point.x > 0) then
181       dx = dx + bbox:width()
182    end
183    if (point.y > 0) then
184       dy = dy + bbox:height()
185    end
186    dx = dx + bbox:left()
187    dy = dy + bbox:bottom()
188    return ipe.Translation(dx, dy)
189 end
190
191 function mainWindow(model)
192    if model.ui.win == nil then
193       return model.ui
194    else
195       return model.ui:win()
196    end
197 end
198
199 function run_decorator(model)
200    -- get bbox of primary selection
201    local p = model:page()
202    local prim = p:primarySelection()
203    if not prim then
204       model.ui:explain("An object must be selected.")
205       return
206    end
207    local bbox = p:bbox(prim)
208
209    -- create decorator object
210    local dialog = ipeui.Dialog(mainWindow(model), "Select a decorator.")
211    local decorators = decorator_names(model)
212    dialog:add("deco", "combo", decorators, 1, 1, 1, 2)
213    dialog:add("ok", "button", { label="&Ok", action="accept" }, 2, 2)
214    dialog:add("cancel", "button", { label="&Cancel", action="reject" }, 2, 1)
215    local r = dialog:execute()
216    if not r then return end
217    local deco_name = decorators[dialog:get("deco")]
218    local symbol = model.doc:sheets():find("symbol", deco_name)
219    local deco = symbol:clone()
220
221    -- run the decoration
222    decorate(model, bbox, deco)
223 end
224
225 label = "Decorator"
226 methods = {
227   { label = "Decorate", run=run_decorator},
228   { label = "Fancy decorator", run=run_fancy_decorator},
229 }