diff --git a/darkdraw/__init__.py b/darkdraw/__init__.py index 0a357db..19d1260 100644 --- a/darkdraw/__init__.py +++ b/darkdraw/__init__.py @@ -1,16 +1,18 @@ from .box import * from .charbrowser import * from .drawing import * -from .animation import * from .upgrade import * +from .stamps import * from .ansihtml import * # save to .ansihtml from .save import * +from .load_ans import * from .save_ans import * from .load_dur import * +from .save_dur import * from .boxdraw import * from .flip import * -from .draw_ops import * +from .frame_mgmt import * from .loader_scr import * # deprecated 2020 format, remove anytime diff --git a/darkdraw/frame_mgmt.ddw b/darkdraw/frame_mgmt.ddw new file mode 100644 index 0000000..627ee67 --- /dev/null +++ b/darkdraw/frame_mgmt.ddw @@ -0,0 +1,54 @@ +{"id": "0", "type": "frame", "x": null, "y": null, "text": "", "color": "", "tags": [], "group": "", "frame": null, "rows": null, "duration_ms": 100, "ref": null} +{"id": "1", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100} +{"id": "2", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100} +{"id": "3", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100} +{"id": "4", "type": "frame", "text": "", "color": "", "tags": [], "group": "", "duration_ms": 100} +{"x": 3, "y": 1, "text": "frame_mgmt.py", "color": "14", "tags": [], "group": "", "frame": ""} +{"x": 33, "y": 3, "text": "Frame ID Linking", "color": "13", "tags": [], "group": "", "frame": "0"} +{"x": 3, "y": 5, "text": "In the backing sheet, changes to the id value of a frame object are propagated", "color": "", "tags": [], "group": "", "frame": "0"} +{"x": 3, "y": 6, "text": "to all text objects referencing that frame id, but not the other way around.", "color": "", "tags": [], "group": "", "frame": "0"} +{"x": 3, "y": 7, "text": "Open a second pane (Z), and open the backing sheet there (`). Edit (e) the", "color": "", "tags": [], "group": "", "frame": "0"} +{"x": 3, "y": 8, "text": "id field of the first item in the backing sheet, type TEST (or anything but 0)", "color": "", "tags": [], "group": "", "frame": "0"} +{"x": 3, "y": 9, "text": "and confirm the edit (Enter). The frame values of all objects on this frame", "color": "", "tags": [], "group": "", "frame": "0"} +{"x": 3, "y": 10, "text": "have changed to match the new frame id. Editing the frame value of these objects", "color": "", "tags": [], "group": "", "frame": "0"} +{"x": 3, "y": 11, "text": "will not affect the frame's id value.", "color": "", "tags": [], "group": "", "frame": "0"} +{"x": 23, "y": 7, "text": "Z", "color": "11", "tags": [], "group": "", "frame": "0"} +{"x": 61, "y": 7, "text": "`", "color": "11", "tags": [], "group": "", "frame": "0"} +{"x": 71, "y": 7, "text": "e", "color": "11", "tags": [], "group": "", "frame": "0"} +{"x": 25, "y": 9, "text": "Enter", "color": "11", "tags": [], "group": "", "frame": "0"} +{"x": 31, "y": 14, "text": "to duplicate-frame ]", "color": "10", "tags": [], "group": "", "frame": "0"} +{"x": 30, "y": 3, "text": "duplicate-frame", "color": "13", "tags": [], "group": "", "frame": "1"} +{"x": 3, "y": 5, "text": "In the canvas, press gz] to create a copy of the current frame.", "color": "", "tags": [], "group": "", "frame": "1"} +{"x": 3, "y": 6, "text": "(gz[ would also work, but later steps in this tutorial assume that you", "color": "", "tags": [], "group": "", "frame": "1"} +{"x": 3, "y": 7, "text": "will have pressed gz] here.)", "color": "", "tags": [], "group": "", "frame": "1"} +{"x": 4, "y": 6, "text": "gz[", "color": "11", "tags": [], "group": "", "frame": "1"} +{"x": 21, "y": 7, "text": "gz]", "color": "11", "tags": [], "group": "", "frame": "1"} +{"x": 24, "y": 5, "text": "gz]", "color": "11", "tags": [], "group": "", "frame": "1"} +{"x": 27, "y": 14, "text": "to combine-duplicates ]", "color": "10", "tags": [], "group": "", "frame": "1"} +{"x": 33, "y": 3, "text": "combine-duplicates", "color": "13", "tags": [], "group": "", "frame": "2"} +{"x": 3, "y": 6, "text": "Press Space, and enter command longname combine-duplicates.", "color": "", "tags": [], "group": "", "frame": "2"} +{"x": 3, "y": 7, "text": "The duplicated items on the previous frame/the new copy are now combined", "color": "", "tags": [], "group": "", "frame": "2"} +{"x": 3, "y": 8, "text": "into single items with two frame ids.", "color": "", "tags": [], "group": "", "frame": "2"} +{"x": 3, "y": 5, "text": "Identical objects on multiple frames can be combined to declutter the backing sheet.", "color": "", "tags": [], "group": "", "frame": "2"} +{"x": 9, "y": 6, "text": "Space", "color": "11", "tags": [], "group": "", "frame": "2"} +{"x": 43, "y": 6, "text": "combine-duplicates", "color": "11", "tags": [], "group": "", "frame": "2"} +{"x": 30, "y": 14, "text": "to self-contain-frames ]", "color": "10", "tags": [], "group": "", "frame": "2"} +{"x": 26, "y": 3, "text": "self-contain-frames", "color": "13", "tags": [], "group": "", "frame": "3"} +{"x": 3, "y": 5, "text": "The opposite can also be done, to prevent edits on a given frame", "color": "", "tags": [], "group": "", "frame": "3"} +{"x": 3, "y": 6, "text": "from affecting the contents of other frames.", "color": "", "tags": [], "group": "", "frame": "3"} +{"x": 3, "y": 7, "text": "Enter command longname self-contain-frames.", "color": "", "tags": [], "group": "", "frame": "3"} +{"x": 3, "y": 8, "text": "The multi-frame objects created from frame 1 have again", "color": "", "tags": [], "group": "", "frame": "3"} +{"x": 3, "y": 9, "text": "become separate single-frame objects", "color": "", "tags": [], "group": "", "frame": "3"} +{"x": 26, "y": 7, "text": "self-contain-frames", "color": "11", "tags": [], "group": "", "frame": "3"} +{"x": 19, "y": 14, "text": "to delete-orphan-frame-elements ]", "color": "10", "tags": [], "group": "", "frame": "3"} +{"x": 21, "y": 3, "text": "delete-orphan-frame-elements", "color": "13", "tags": [], "group": "", "frame": "4"} +{"x": 3, "y": 6, "text": "In the backing sheet, delete ONLY the frame object for frame 1-2.", "color": "", "tags": [], "group": "", "frame": "4"} +{"x": 3, "y": 7, "text": "Then, enter command longname delete-orphan-frame-elements.", "color": "", "tags": [], "group": "", "frame": "4"} +{"x": 3, "y": 8, "text": "The text objects from frame 1-2 have been pruned.", "color": "", "tags": [], "group": "", "frame": "4"} +{"x": 3, "y": 5, "text": "Objects that are unused by any frame can be trimmed, all at once.", "color": "", "tags": [], "group": "", "frame": "4"} +{"x": 64, "y": 6, "text": "1-2", "color": "11", "tags": [], "group": "", "frame": "4"} +{"x": 32, "y": 7, "text": "delete-orphan-frame-elements", "color": "11", "tags": [], "group": "", "frame": "4"} +{"x": 31, "y": 8, "text": "1-2", "color": "11", "tags": [], "group": "", "frame": "4"} +{"x": 32, "y": 10, "text": "__", "color": "9", "tags": [], "group": "", "frame": "4"} +{"x": 31, "y": 11, "text": "/_' _/", "color": "9", "tags": [], "group": "", "frame": "4"} +{"x": 30, "y": 12, "text": "/_,/)(/o", "color": "9", "tags": [], "group": "", "frame": "4"} diff --git a/darkdraw/frame_mgmt.py b/darkdraw/frame_mgmt.py new file mode 100644 index 0000000..3180050 --- /dev/null +++ b/darkdraw/frame_mgmt.py @@ -0,0 +1,169 @@ +from copy import copy as _copy + +from visidata import vd, VisiData, ItemColumn + +from .drawing import DrawingSheet, Drawing + + +def _link_id_setter(col, row, val): + old = getattr(row, 'id', None) + if not (getattr(row, 'type', '') == 'frame' and old and old != val): + return + n = 0 + for r in col.sheet.rows: + if r is row or getattr(r, 'type', '') or not getattr(r, 'frame', ''): continue + ids = r.frame.split() + if old not in ids: continue + vd.addUndo(setattr, r, 'frame', r.frame) + r.frame = ' '.join(val if x == old else x for x in ids) + n += 1 + if n: vd.status(f'link_frame_ids: updated {n} element(s) {old!r} → {val!r}') + + +for _i, _c in enumerate(DrawingSheet.columns): + if _c.name == 'id': + DrawingSheet.columns[_i] = ItemColumn('id', type=str, setter=_link_id_setter) + break + + +### DUPLICATE-FRAME ########################################################## +@Drawing.api +def duplicate_frame(sheet, before=False): + src = sheet.source + if not src.frames: + return src.new_between_frame(-1, 0) + + cur = src.frames[sheet.cursorFrameIndex] + if before: + adj = src.frames[sheet.cursorFrameIndex-1] if sheet.cursorFrameIndex > 0 else None + if adj: + base = str(adj.id)+'-'+str(cur.id) + else: + try: base = str(int(cur.id)-1) + except ValueError: base = f'{cur.id}-pre' + else: + adj = src.frames[sheet.cursorFrameIndex+1] if sheet.cursorFrameIndex+1 < len(src.frames) else None + if adj: + base = str(cur.id)+'-'+str(adj.id) + else: + try: base = str(int(cur.id)+1) + except ValueError: base = f'{cur.id}-post' + + existing = {r.id for r in src.rows if r.type == 'frame'} + name = base + i = 2 + while name in existing: + name = f'{base}-{i}' + i += 1 + + newf = src.newRow() + newf.type = 'frame' + newf.id = name + newf.duration_ms = cur.duration_ms or 100 + + for i, r in enumerate(src.rows): + if r is cur: + vd.clearCaches() + src.addRow(newf, index=i if before else i+1) + break + + dup_rows = [_copy(r) for r in src.rows if cur.id in (r.frame or '').split()] + for r in dup_rows: + r.frame = newf.id + src.addRow(r) + return newf + +Drawing.addCommand('gz[', 'duplicate-frame-before', 'sheet.duplicate_frame(before=True)', 'insert new frame before current with copy of current frame objects') +Drawing.addCommand('gz]', 'duplicate-frame-after', 'sheet.duplicate_frame(before=False); sheet.cursorFrameIndex += 1', 'insert new frame after current with copy of current frame objects') +############################################################################## + +### DELETE ORPHAN-FRAME ELEMENTS ############################################## +@DrawingSheet.api +def delete_orphan_frame_elements(self): + 'Delete elements whose .frame references no existing frame id.' + valid = {r.id for r in self.rows if r.type == 'frame'} + orphans = [r for r in self.rows + if not r.type and r.frame + and not (set(r.frame.split()) & valid)] + if not orphans: + vd.status('no orphan-frame elements') + return + oids = set(map(id, orphans)) + self.deleteBy(lambda r, oids=oids: id(r) in oids) + vd.status(f'deleted {len(orphans)} orphan-frame element(s)') + +DrawingSheet.addCommand(None, 'delete-orphan-frame-elements', 'sheet.delete_orphan_frame_elements()', 'delete elements whose frame field references no existing frame') +Drawing.addCommand(None, 'delete-orphan-frame-elements', 'sheet.source.delete_orphan_frame_elements()', 'delete elements whose frame field references no existing frame') +############################################################################## + +### COMBINE DUPLICATES ####################################################### +@DrawingSheet.api +def combine_duplicates(self): + 'Merge elements sharing x,y,text,color,tags,group: union their frame ids onto one, delete the rest.' + groups = {} + for r in self.rows: + if r.type: continue + key = (r.x, r.y, r.text, r.color, frozenset(r.tags or []), r.group) + groups.setdefault(key, []).append(r) + + to_delete = [] + merged = 0 + for rs in groups.values(): + if len(rs) < 2: continue + frames = set() + any_empty = False + for r in rs: + if not r.frame: + any_empty = True + else: + frames.update(r.frame.split()) + keeper = rs[0] + new_frame = '' if any_empty else ' '.join(sorted(frames)) + if keeper.frame != new_frame: + vd.addUndo(setattr, keeper, 'frame', keeper.frame) + keeper.frame = new_frame + to_delete.extend(rs[1:]) + merged += 1 + + if not to_delete: + vd.status('no duplicates') + return + oids = set(map(id, to_delete)) + self.deleteBy(lambda r, oids=oids: id(r) in oids) + vd.status(f'combined {merged} group(s), removed {len(to_delete)} duplicate(s)') + +DrawingSheet.addCommand(None, 'combine-duplicates', 'sheet.combine_duplicates()', 'merge elements with same x,y,text,color,tags,group: union frame ids, delete extras') +Drawing.addCommand(None, 'combine-duplicates', 'sheet.source.combine_duplicates()', 'merge elements with same x,y,text,color,tags,group: union frame ids, delete extras') +############################################################################## + +### SELF-CONTAIN FRAMES ###################################################### +@DrawingSheet.api +def self_contain_frames(self): + 'Split each element with >1 frame ids into one copy per frame; delete originals. Skips empty/null frame field.' + to_delete = [] + new_rows = [] + n_split = 0 + for r in list(self.rows): + if r.type: continue + if not r.frame: continue + ids = r.frame.split() + if len(ids) < 2: continue + for fid in ids: + new_r = _copy(r) + new_r.frame = fid + self.addRow(new_r) + new_rows.append(new_r) + to_delete.append(r) + n_split += 1 + if not to_delete: + vd.status('no multi-frame elements') + return + new_ids = set(map(id, new_rows)) + vd.addUndo(self.deleteBy, lambda r, ids=new_ids: id(r) in ids) + oids = set(map(id, to_delete)) + self.deleteBy(lambda r, oids=oids: id(r) in oids) + vd.status(f'split {n_split} element(s) into {len(new_rows)} copies') + +DrawingSheet.addCommand(None, 'self-contain-frames', 'sheet.self_contain_frames()', 'split each multi-frame element into one copy per frame; delete originals') +Drawing.addCommand(None, 'self-contain-frames', 'sheet.source.self_contain_frames()', 'split each multi-frame element into one copy per frame; delete originals') +##############################################################################