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