# -*- coding: utf-8 -*- # Copyright (C) 2003-2006 by Igor E. Novikov # Copyright (C) 1997, 1998, 1999 by Bernhard Herzog # # This library is covered by GNU Library General Public License. # For more info see COPYRIGHTS file in sK1 root directory. # # The Blend Group # from types import IntType, ListType import operator #from app.UI import command from app.conf.const import SelectAdd from app import SketchInternalError, _ from app import NullUndo, CreateListUndo, CreateMultiUndo, Undo, UndoAfter import selinfo from compound import Compound from blend import Blend from group import Group SelectStart = 0 SelectEnd = 1 class BlendInterpolation(Compound): can_be_empty = 1 is_BlendInterpolation = 1 def __init__(self, steps = 2, start = None, end = None, duplicate = None): if duplicate is not None: self.steps = duplicate.steps Compound.__init__(self, duplicate = duplicate) else: self.steps = steps if start is not None: Compound.__init__(self, self.compute_blend(start, end)) else: Compound.__init__(self) def SetParameters(self, steps): if steps < 2: raise ValueError, 'steps must be >= 2' if self.steps != steps: undo = (self.SetParameters, self.steps) self.steps = steps self.parent.RecomputeChild(self) else: undo = NullUndo return undo def Steps(self): return self.steps def Recompute(self, start, end): self.set_objects(self.compute_blend(start, end)) def compute_blend(self, start, end): steps = self.steps blended = [] for step in range(1, steps): fraction = float(step) / steps blend = Blend(start, end, fraction) blend.SetDocument(self.document) blend.SetParent(None) blended.append(blend) blended.reverse() return blended def set_objects(self, new_objs): # called by __init__ and recompute if self.document is not None: self.document.AddClearRect(self.bounding_rect) for obj in self.objects: obj.Destroy() self.objects = new_objs self.del_lazy_attrs() if self.document is not None: self.document.AddClearRect(self.bounding_rect) self.issue_changed() def SaveToFile(self, file): if file.options.full_blend: file.BeginBlendInterpolation(self.steps) for obj in self.objects: obj.SaveToFile(file) file.EndBlendInterpolation() else: file.BlendInterpolation(self.steps) def AsGroup(self): return Group(self.objects[:]) def Transform(self, trafo): if self.parent.changing_children: return Compound.Transform(self, trafo) else: return NullUndo def Translate(self, offset): if self.parent.changing_children: Compound.Translate(self, offset) return NullUndo def Info(self): return _("Interpolation with %d steps") % self.steps class BlendGroup(Compound): is_Blend = 1 is_Group = 1 allow_traversal = 1 # allow selection of subobjects via M-Down etc. def __init__(self, steps = 0, start = None, end = None, duplicate = None): # Three different ways to instantiate this class: # # 1. Duplicating a BlendGroup: duplicate is not None. # # 2. Creating a BlendGroup from two normal graphics objects: # start and end are not None. # # 3. Creating a BlendGroup from an sk file: steps is 0. if duplicate is not None: # case 1 Compound.__init__(self, duplicate = duplicate) #self.Connect() elif start is not None: # case 2 inter = BlendInterpolation(steps, start, end) Compound.__init__(self, [start, inter, end]) #self.Connect() else: # case 3 Compound.__init__(self) def load_AppendObject(self, object): Compound.load_AppendObject(self, object) length = len(self.objects) if length > 2 and length & 1: # last object was a control object if len(self.objects[-2].objects) == 0: self.objects[-2].Recompute(self.objects[-3], self.objects[-1]) def insert(self, obj, at): undo_info = None try: if type(at) != IntType or at > len(self.objects): at = len(self.objects) self.objects.insert(at, obj) obj.SetDocument(self.document) obj.SetParent(self) obj.Connect() undo_info = (self.remove, at) self._changed() return undo_info except: if undo_info is not None: Undo(undo_info) raise def remove(self, idx): obj = self.objects[idx] del self.objects[idx] obj.Disconnect() obj.SetParent(None) self._changed() return (self.insert, obj, idx) def do_remove_child(self, idx): undo = [] try: if idx % 2 == 0: # a control object if self.document is not None: undo.append(self.document.AddClearRect(self.bounding_rect)) if len(self.objects) > 3: if idx == 0: undo.append(self.remove(1)) undo.append(self.remove(0)) elif idx == len(self.objects) - 1: undo.append(self.remove(idx)) undo.append(self.remove(idx - 1)) else: steps = self.objects[idx + 1].Steps() \ + self.objects[idx - 1].Steps() u = (UndoAfter, CreateMultiUndo(self.remove(idx + 1), self.remove(idx)), self.objects[idx - 1].SetParameters(steps)) undo.append(u) else: # remove one of only two control objects -> Remove self undo.append(self.parent.Remove(self)) return CreateListUndo(undo) else: # XXX implement this case raise ValueError, 'BlendGroup: cannot remove non control child' except: Undo(CreateListUndo(undo)) raise def RemoveSlice(self, min, max): raise SketchInternalError('RemoveSlice not allowed for BlendGroup') def RemoveObjects(self, infolist): if not infolist: return NullUndo sliced = selinfo.list_to_tree_sliced(infolist) sliced.reverse() undo = [self.begin_change_children()] try: for start, end in sliced: if type(end) == IntType: # > 1 adjacent children of self. XXX implement this raise SketchInternalError('Deletion of multiple objects' ' of BlendGroups not yet' ' implemented') elif type(end) == ListType: # remove grandchildren (children of child of self) if start % 2 == 0: # control object changes. This should result in # a recompute() automatically. undo.append(self.objects[start].RemoveObjects(end)) else: pass else: # a single child. If it's one of our control # objects, remove self undo.append(self.do_remove_child(start)) undo.append(self.end_change_children()) return CreateListUndo(undo) except: Undo(CreateListUndo(undo)) raise def permute_objects(self, permutation): # for now, silently ignore this return NullUndo def DuplicateObjects(self, infolist, offset): # XXX: should allow duplication of the control objects by inserting # them into the parent. return [], NullUndo def ReplaceChild(self, child, object): idx = self.objects.index(child) if idx % 2 == 0: # the object is a control object undo = self.ReplaceChild, object, child self.objects[idx] = object object.SetParent(self) object.SetDocument(self.document) child.SetParent(None) self.ChildChanged(object) return undo else: raise SketchError('Cannot replace child') def SelectSubobject(self, p, rect, device, path = None, *rest): idx = self.Hit(p, rect, device) - 1 obj = self.objects[idx] if idx % 2 == 0: # a control object if path: path_idx = path[0] path = path[1:] obj = obj.SelectSubobject(p, rect, device, path) elif path == (): obj = obj.SelectSubobject(p, rect, device) info = selinfo.prepend_idx(idx, obj) else: # an interpolation object if path is None: info = self else: info = selinfo.prepend_idx(idx, obj) return info def ForAllUndo(self, func): # XXX: should we just change start and end and recompute? self.begin_change_children() undo = map(func, self.objects) self.end_change_children(ignore_child_changes = 1) idx = range(0, len(self.objects), 2) undo = map(operator.getitem, [undo] * len(idx), idx) return CreateListUndo(undo) def begin_change_children(self): self.child_has_changed = 0 return Compound.begin_change_children(self) def end_change_children(self, ignore_child_changes = 0): if self.child_has_changed and not ignore_child_changes: self.document.AddAfterHandler(self.recompute, (), self.depth()) self.child_has_changed = 0 return Compound.end_change_children(self) def ChildChanged(self, child): idx = self.objects.index(child) if idx % 2 == 0: if self.changing_children: self.child_has_changed = 1 else: depth = self.depth(); recompute = self.recompute if idx > 0: self.document.AddAfterHandler(recompute, (idx - 1,), depth) if idx < len(self.objects) - 1: self.document.AddAfterHandler(recompute, (idx + 1,), depth) else: Compound.ChildChanged(self, child) self.del_lazy_attrs() def recompute(self, idx): self.objects[idx].Recompute(self.objects[idx - 1], self.objects[idx+1]) def RecomputeChild(self, child): self.recompute(self.objects.index(child)) def SaveToFile(self, file): file.BeginBlendGroup() for obj in self.objects: obj.SaveToFile(file) file.EndBlendGroup() def Info(self): return _("BlendGroup with %d control objects") \ % (len(self.objects) / 2 + 1) def CancelEffect(self): self.unset_parent() idx = range(0, len(self.objects), 2) return map(operator.getitem, [self.objects] * len(idx), idx) def SelectControl(self, relative, which): idx = self.objects.index(relative) if idx % 2 == 1: # an interpolation if which == SelectStart: idx = idx - 1 else: idx = idx + 1 else: # a control if which == SelectStart: if idx > 0: idx = idx - 2 else: if idx < len(self.objects) - 1: idx = idx + 2 self.document.SelectObject(self.objects[idx]) def ExtendBlend(self, start, end, steps): idx = self.objects.index(start) if idx == 0: inter = BlendInterpolation(steps, end, start) return CreateMultiUndo(self.insert(inter, 0), self.insert(end, 0)) elif idx == len(self.objects) - 1: inter = BlendInterpolation(steps, start, end) return CreateMultiUndo(self.insert(inter, None), self.insert(end, None)) def Ungroup(self): objects = [] for obj in self.objects: if obj.__class__ is BlendInterpolation: objects.append(obj.AsGroup()) else: objects.append(obj) return objects def CreateBlendGroup(start, end, steps): if isinstance(start.parent, BlendGroup): undo = start.parent.ExtendBlend(start, end, steps) result = start.parent elif isinstance(end.parent, BlendGroup): undo = end.parent.ExtendBlend(end, start, steps) result = end.parent else: result = BlendGroup(steps, start, end) undo = NullUndo return result, undo