Cleanup panel

Drawing Cleanup

Purge, audit, delete zero-length and duplicate geometry and zoom extents in one run, from a checklist that remembers your last choice.

On the ribbonButton, Cleanup panel
NeedsAn open drawing (the button is greyed without one)
CommandPYDRAWINGCLEANUP, PYDRAWINGCLEANUPCFG for Shift+Click
AliasDCU
Shift+ClickResets the checklist to its default ticks

What it does#

Opens a checklist of six steps, runs the ticked ones in order, and reports what each one did. The checklist opens with whatever you ticked last time.

StepTicked at firstWhat it does
Purge everything unusedYes Erases unreferenced blocks, layers, linetypes, text styles, dimension styles, registered applications, multiline styles, table styles and multileader styles. It repeats until a pass finds nothing, so a block that was only used inside another unused block goes too.
Audit the drawing and fix errorsYes Queues AUDIT with fix set to Yes.
Delete zero-length lines and polylinesYes Erases lines and lightweight polylines with no length, in model space and every layout.
Delete duplicate entitiesYes Finds entities of the same type, on the same layer and in the same space, with the same geometry, and erases every copy but the first.
Set every entity colour to ByLayerNo Sets the colour of every entity in model space and the layouts, and of the attributes on block references, to ByLayer.
Zoom extentsYes Queues ZOOM Extents.

How to use it#

  1. Click Drawing Cleanup, or type PYDRAWINGCLEANUP.
  2. Tick the steps you want and click OK. The choice is remembered for next time (an empty checklist is not).
  3. If a ticked step erases something (purge, zero-length, duplicates), the tool says exactly what it will delete and asks you to continue.
  4. A progress window follows the steps. Cancel stops the step that is running and skips the rest; whatever already finished stays done.
  5. A report window lists one line per step, for example:
    Purge: 14 unused items removed
    Audit: queued; it runs when this tool finishes and reports on the command line
    Zero-length: 3 entities deleted
    Duplicates: 12 extra copies deleted (9 groups), 2 skipped on locked layers
    Zoom extents: queued
    Copy puts the report on the clipboard. A toast sums it up, such as "14 purged, 3 zero-length deleted, 12 duplicates deleted".
  6. When the tool closes, AUDIT runs and prints its findings on the command line, then the view zooms to extents.

Limitations#

  • AUDIT and ZOOM do not run in checklist order: they are queued as commands of their own and run after every other step has finished. Their results are on the command line, not in the report.
  • Duplicates are judged by type, layer and geometry only. Colour, linetype, lineweight, text style and text rotation are not compared, so two lines in the same place on the same layer are duplicates even if one is red and dashed. Polylines are compared by their vertices and whether they are closed, not by arc segments or widths. Only lines, circles, arcs, lightweight polylines, text, mtext and block references are checked; a block reference's attribute values are part of its match, so two tags on one point with different values are both kept.
  • Zero-length covers lines and lightweight polylines only, not old-style 2D or 3D polylines.
  • Entities on locked layers are never erased or recoloured: the report counts them as skipped. Purge is not affected by layer locks.
  • Geometry inside block definitions is not checked for duplicates or zero length, and ByLayer does not change the colours inside block definitions. Viewports keep their colour.
  • Purge does not cover materials, visual styles, plot styles, groups or empty text. Use AutoCAD's PURGE for those.

Good to know#

Every edit the tool makes itself happens inside its own command, so U takes it back. AUDIT and ZOOM are separate commands that run after it, so they come off the undo stack first: undo those two, then the cleanup itself.

The remembered checklist is stored in %APPDATA%\pyACAD\settings\cfg_pyacad_tab_cleanup_panel_drawing_cleanup_pushbutton.json. Shift+Click (or PYDRAWINGCLEANUPCFG) deletes it, so the next run opens with the default ticks again: everything but ByLayer colour.

Other uses#

  • Before you issue a drawing, or before it goes to someone as an xref background.
  • Tick only Set every entity colour to ByLayer to make a consultant's background follow your layer colours.
  • Tick only Delete duplicate entities after a careless copy and paste in place.

Source#

The tool's own files, word for word as shipped in Cleanup.panel/Drawing_Cleanup.pushbutton. Every pyACAD tool is an ordinary bundle: copy these into your own extension and change what you like.

script.py Runs on a click. View on GitHub
# -*- coding: utf-8 -*-
"""Drawing hygiene in one run: purge, audit, zero-length and duplicate
geometry, ByLayer colour if asked, zoom extents.

A checklist picks the steps and remembers the last choice (Shift+Click
forgets it). Steps that erase something are confirmed first. Every edit this
tool makes itself happens inside its own command, so Undo takes it back.
AUDIT and ZOOM are queued as commands of their own and run once the tool
finishes, so AUDIT's findings are on the command line rather than in the
report. Entities on locked layers are never touched.
"""

from pyacad import audit, doc, forms, script, toast

__title__ = 'Drawing Cleanup'

TITLE = 'Drawing Cleanup'
DEFAULTS = {'steps': None}          # None: nothing remembered yet, so tick the defaults

WHAT_IT_ERASES = {
    'purge': 'purge unused blocks, layers, linetypes and styles',
    'zero_length': 'delete zero-length lines and polylines',
    'duplicates': 'delete duplicate entities, keeping one of each',
}


def has_drawing():
    try:
        doc.active()
    except RuntimeError:
        return False
    return True


def choose_steps():
    """The ticked steps in run order, or None when the checklist was cancelled."""
    saved = script.get_config(DEFAULTS)
    picked = forms.select_from_list(
        [(label, key) for key, label, _ in audit.STEPS],
        title=TITLE, multiselect=True,
        prompt='Tick the steps to run. They run top to bottom.',
        checked=audit.cleanup_plan(saved['steps']))
    if picked is None:
        return None
    plan = audit.cleanup_plan(picked)
    if plan:
        # An empty checklist is not worth remembering: the next run would
        # open with nothing ticked.
        script.save_config({'steps': plan})
    return plan


def confirmed(plan):
    """Asks before anything is erased; True to go ahead."""
    erasing = [WHAT_IT_ERASES[step] for step in audit.deleting_steps(plan)]
    if not erasing:
        return True
    if len(erasing) > 1:
        what = ', '.join(erasing[:-1]) + ' and ' + erasing[-1]
    else:
        what = erasing[0]
    return forms.confirm(
        'This will %s.\n\nEntities on locked layers are left alone, and Undo '
        'takes the changes back.\n\nContinue?' % what, TITLE)


class Watch(object):
    """Progress and cancel callbacks for one step's share of the bar."""

    def __init__(self, bar, start, share):
        self.bar = bar
        self.start = start
        self.share = share
        self.stopped = False

    def progress(self, done, total):
        if not self.bar.update(self.start + self.share * float(done) / max(total, 1)):
            self.stopped = True

    def cancelled(self):
        return self.stopped


def run_step(step, watch):
    """Runs one step; returns its result for audit.format_report()."""
    if step == 'purge':
        return audit.purge(all=True)
    if step == 'audit':
        audit.audit(fix=True)
        return None                                     # queued
    if step == 'zero_length':
        handles = audit.zero_length(watch.progress, watch.cancelled)
        if watch.stopped:
            return {'cancelled': True}
        erased, skipped = audit.erase(handles)
        return {'count': erased, 'skipped': skipped}
    if step == 'duplicates':
        groups = audit.duplicates(progress=watch.progress, cancelled=watch.cancelled)
        if watch.stopped:
            return {'cancelled': True}
        erased, skipped = audit.erase(audit.duplicate_extras(groups))
        return {'count': erased, 'skipped': skipped, 'groups': len(groups)}
    if step == 'bylayer':
        changed, skipped = audit.colors_to_bylayer(watch.progress, watch.cancelled)
        if watch.stopped:
            return {'cancelled': True}
        return {'count': changed, 'skipped': skipped}
    if step == 'zoom':
        doc.send_command('_.ZOOM _E')
        return None                                     # queued
    raise ValueError('Unknown step %s' % step)


def run_steps(plan):
    """Runs the plan in order; returns [(step, result)]. A failed step is
    reported and the rest still run; Cancel stops the rest."""
    results = []
    share = 1.0 / len(plan)
    with forms.progress(TITLE, audit.step_label(plan[0])) as bar:
        for index, step in enumerate(plan):
            if not bar.update(index * share, audit.step_label(step)):
                results.extend((rest, {'not_run': True}) for rest in plan[index:])
                break
            try:
                result = run_step(step, Watch(bar, index * share, share))
            except Exception as error:
                script.get_logger().error('%s step failed: %s' % (step, error))
                result = {'error': str(error)}
            results.append((step, result))
            if isinstance(result, dict) and result.get('cancelled'):
                results.extend((rest, {'not_run': True}) for rest in plan[index + 1:])
                break
    return results


def run():
    if not has_drawing():
        toast.warning('No drawing is open', 'Open a drawing, then run Drawing Cleanup')
        return

    plan = choose_steps()
    if plan is None:
        return                                          # cancelled: say nothing
    if not plan:
        toast.info('Nothing to do', 'No steps were ticked')
        return
    if not confirmed(plan):
        return

    results = run_steps(plan)
    report = audit.format_report(results)
    forms.alert(report, TITLE, copy=report)

    line = audit.summary(results)
    if any(isinstance(result, dict) and result.get('error') for _, result in results):
        toast.warning('Drawing Cleanup hit a problem', line)
    else:
        toast.success('Drawing cleaned', line)


if '__commandpath__' in globals():
    run()
config.py Runs on Shift+Click. View on GitHub
# -*- coding: utf-8 -*-
"""Shift+Click: forgets the steps Drawing Cleanup remembered, so its next
run opens with the default steps ticked again (everything but ByLayer
colour)."""

from pyacad import script, toast


def run():
    try:
        script.reset_config()
    except (IOError, OSError) as error:
        toast.error('Could not reset Drawing Cleanup', str(error))
        return
    toast.success('Drawing Cleanup reset', 'The next run opens with the default steps ticked')


if '__commandpath__' in globals():
    run()
bundle.yaml The bundle contract: the keys pyACAD reads for this tool. View on GitHub
title: Drawing Cleanup
tooltip: Purges, audits, deletes zero-length and duplicate geometry and zooms to extents in one run, from a checklist that remembers your last choice. Shift+Click resets the checklist to its defaults.
context: doc
keytip: CC
alias: DCU