top of page

Bulk Import - SketchUp .DAE to Blender

The SketchUp .dae Importer is a Blender add-on designed for batch importing .dae files exported from SketchUp, organizing them into collections, and cleaning up unnecessary objects. It streamlines workflows by automatically joining geometry, moving extras like curves and empties into separate collections, and eliminating imported cameras.

Bulk Import - SketchUp .DAE to Blender

Broken button... I'll fix it eventually...

bl_info = {

    "name": "SketchUp Tag DAE Importer",

    "author": "Spencer Clem & ChatGPT",

    "version": (3, 2, 0),

    "blender": (3, 0, 0),

    "location": "View3D > Sidebar > SketchUp tab",

    "description": "Import SketchUp 'one DAE per Tag' exports. One object per DAE, organized into a timestamped collection per import run, plus Extras.",

    "support": "COMMUNITY",

    "category": "Import-Export",

}


import bpy

import os

import re

from datetime import datetime

from bpy.props import (

    StringProperty,

    PointerProperty,

    BoolProperty,

    EnumProperty,

    FloatProperty,

)

from bpy.types import Operator, Panel, PropertyGroup



# ----------------------------

# Helpers

# ----------------------------


def _safe_name_from_filename(path: str) -> str:

    base = os.path.splitext(os.path.basename(path))[0]

    base = re.sub(r'[:"/\\|?*]', "_", base)

    base = base.strip() or "Untagged"

    return base



def _ensure_collection(name: str, parent: bpy.types.Collection = None) -> bpy.types.Collection:

    col = bpy.data.collections.get(name)

    if col is None:

        col = bpy.data.collections.new(name)

        if parent:

            parent.children.link(col)

        else:

            bpy.context.scene.collection.children.link(col)

    return col



def _ensure_child_collection_unique(parent: bpy.types.Collection, base_name: str) -> bpy.types.Collection:

    """

    Create a child collection under `parent` with a name based on base_name.

    If a collection with that name already exists, append " (2)", " (3)", etc.

    """

    # Check existing child names (not global collection names)

    existing_names = {c.name for c in parent.children}

    if base_name not in existing_names:

        col = bpy.data.collections.new(base_name)

        parent.children.link(col)

        return col


    i = 2

    while True:

        candidate = f"{base_name} ({i})"

        if candidate not in existing_names:

            col = bpy.data.collections.new(candidate)

            parent.children.link(col)

            return col

        i += 1



def _move_object_to_collection(obj: bpy.types.Object, target: bpy.types.Collection):

    if target not in obj.users_collection:

        target.objects.link(obj)

    for col in list(obj.users_collection):

        if col != target:

            try:

                col.objects.unlink(obj)

            except RuntimeError:

                pass



def _deselect_all():

    for obj in bpy.context.selected_objects:

        obj.select_set(False)



def _join_meshes(mesh_objects):

    if not mesh_objects:

        return None

    _deselect_all()

    for obj in mesh_objects:

        obj.select_set(True)

    bpy.context.view_layer.objects.active = mesh_objects[0]

    bpy.ops.object.join()

    return bpy.context.view_layer.objects.active



def _apply_scale(obj: bpy.types.Object):

    _deselect_all()

    obj.select_set(True)

    bpy.context.view_layer.objects.active = obj

    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)



def _set_origin_to_geometry(obj: bpy.types.Object):

    _deselect_all()

    obj.select_set(True)

    bpy.context.view_layer.objects.active = obj

    bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY", center="BOUNDS")



def _remove_object(obj: bpy.types.Object):

    try:

        bpy.data.objects.remove(obj, do_unlink=True)

    except RuntimeError:

        try:

            obj.user_clear()

            bpy.data.objects.remove(obj)

        except Exception:

            pass



# ----------------------------

# Properties

# ----------------------------


class SketchUpDAEImportProperties(PropertyGroup):

    dae_directory: StringProperty(

        name="DAE Directory",

        description="Directory containing the per-Tag .dae files exported from SketchUp",

        default="",

        subtype="DIR_PATH",

    )


    organization_mode: EnumProperty(

        name="Organization",

        description="How to handle meshes per file",

        items=[

            ("ONE_OBJECT_PER_FILE", "One object per DAE (recommended)", "Join meshes per file into one object"),

            ("KEEP_AS_IS", "Keep imported objects", "Do not join meshes; just organize"),

        ],

        default="ONE_OBJECT_PER_FILE",

    )


    remove_cameras: BoolProperty(

        name="Remove Cameras",

        description="Remove cameras imported by Collada",

        default=True,

    )


    move_extras: BoolProperty(

        name="Move Empties/Curves to Extras",

        description="Move empties and curves to an Extras collection",

        default=True,

    )


    apply_unit_scale: BoolProperty(

        name="Scale Fix",

        description="Apply a scale factor to imported geometry (use if your DAE imports at wrong size)",

        default=False,

    )


    scale_factor: FloatProperty(

        name="Scale Factor",

        description="Multiply imported geometry by this value before applying transforms (only if Scale Fix enabled)",

        default=1.0,

        min=0.000001,

        soft_max=1000.0,

    )


    apply_transforms: BoolProperty(

        name="Apply Transforms",

        description="Apply scale to 1,1,1 after import/join (recommended)",

        default=True,

    )


    origin_to_geometry: BoolProperty(

        name="Origin to Geometry",

        description="Set each imported object origin to its bounds center",

        default=False,

    )


    skip_empty_mesh: BoolProperty(

        name="Skip Empty Meshes",

        description="If a DAE produces no mesh objects, skip creating an object",

        default=True,

    )


    sort_files: BoolProperty(

        name="Sort Files",

        description="Sort DAE files alphabetically before import",

        default=True,

    )



# ----------------------------

# Operator

# ----------------------------


class SketchUpDAEImportOperator(Operator):

    bl_idname = "import.sketchup_tag_dae_import"

    bl_label = "Import Tag DAEs"

    bl_description = "Import SketchUp per-Tag DAE exports into a timestamped collection under 'SketchUp Importer'"

    bl_options = {"REGISTER", "UNDO"}


    def execute(self, context):

        props = context.scene.sketchup_dae_import_props

        dae_directory = bpy.path.abspath(props.dae_directory)


        if not os.path.isdir(dae_directory):

            self.report({"ERROR"}, "Invalid directory path")

            return {"CANCELLED"}


        dae_files = [f for f in os.listdir(dae_directory) if f.lower().endswith(".dae")]

        if props.sort_files:

            dae_files.sort(key=lambda s: s.lower())


        if not dae_files:

            self.report({"WARNING"}, "No .dae files found in the directory")

            return {"CANCELLED"}


        # Root collection

        root_collection = _ensure_collection("SketchUp Importer")


        # Extras sibling (unchanged)

        extras_collection = _ensure_collection("Extras", parent=root_collection) if props.move_extras else None


        # NEW: create timestamped sibling collection for THIS import run

        # Format requested: yyyy-mm-dd with time hours and minutes

        stamp = datetime.now().strftime("%Y-%m-%d %H-%M")

        run_collection = _ensure_child_collection_unique(root_collection, stamp)


        imported_count = 0

        skipped_count = 0


        for dae_file in dae_files:

            file_path = os.path.join(dae_directory, dae_file)

            base_name = _safe_name_from_filename(file_path)


            existing_objects = set(bpy.data.objects)


            try:

                bpy.ops.wm.collada_import(filepath=file_path)

            except Exception as e:

                self.report({"WARNING"}, f"Failed to import {dae_file}: {e}")

                skipped_count += 1

                continue


            imported_objects = [obj for obj in bpy.data.objects if obj not in existing_objects]

            if not imported_objects:

                skipped_count += 1

                continue


            mesh_objs = []

            extras = []

            cameras = []


            for obj in imported_objects:

                if obj.type == "CAMERA":

                    cameras.append(obj)

                elif obj.type in {"EMPTY", "CURVE"}:

                    extras.append(obj)

                elif obj.type == "MESH":

                    mesh_objs.append(obj)

                else:

                    extras.append(obj)


            # Remove cameras

            if props.remove_cameras:

                for cam in cameras:

                    _remove_object(cam)


            # Move extras

            if props.move_extras and extras_collection:

                for ex in extras:

                    _move_object_to_collection(ex, extras_collection)

            else:

                for ex in extras:

                    _move_object_to_collection(ex, run_collection)


            # Skip if no mesh (optional)

            if props.skip_empty_mesh and not mesh_objs:

                skipped_count += 1

                continue


            result_objects = []


            if props.organization_mode == "ONE_OBJECT_PER_FILE" and mesh_objs:

                joined = _join_meshes(mesh_objs)

                if joined:

                    joined.name = base_name

                    result_objects = [joined]

            else:

                result_objects = mesh_objs

                for i, obj in enumerate(result_objects):

                    obj.name = f"{base_name}_{i:02d}" if len(result_objects) > 1 else base_name


            # Optional scale fix

            if props.apply_unit_scale and props.scale_factor != 1.0:

                for obj in result_objects:

                    obj.scale = (

                        obj.scale[0] * props.scale_factor,

                        obj.scale[1] * props.scale_factor,

                        obj.scale[2] * props.scale_factor,

                    )


            # Apply transforms

            if props.apply_transforms:

                for obj in result_objects:

                    _apply_scale(obj)


            # Origin

            if props.origin_to_geometry:

                for obj in result_objects:

                    _set_origin_to_geometry(obj)


            # NEW: Move primary objects into the timestamped run collection

            for obj in result_objects:

                _move_object_to_collection(obj, run_collection)


            imported_count += 1


        self.report({"INFO"}, f"Imported to '{run_collection.name}'. Processed {imported_count} file(s). Skipped {skipped_count}.")

        return {"FINISHED"}



# ----------------------------

# Panel

# ----------------------------


class SketchUpDAEImportPanel(Panel):

    bl_idname = "VIEW3D_PT_sketchup_tag_dae_import"

    bl_label = "SketchUp Tag DAE Importer"

    bl_space_type = "VIEW_3D"

    bl_region_type = "UI"

    bl_category = "SketchUp"


    @classmethod

    def poll(cls, context):

        return context.mode in {"OBJECT"}


    def draw(self, context):

        layout = self.layout

        props = context.scene.sketchup_dae_import_props


        layout.prop(props, "dae_directory")


        layout.separator()


        col = layout.column(align=True)

        col.label(text="Import + Organization")

        col.prop(props, "organization_mode")

        col.prop(props, "sort_files")


        layout.separator()


        col = layout.column(align=True)

        col.label(text="Cleanup")

        col.prop(props, "remove_cameras")

        col.prop(props, "move_extras")

        col.prop(props, "skip_empty_mesh")


        layout.separator()


        col = layout.column(align=True)

        col.label(text="Scale / Transforms")

        col.prop(props, "apply_unit_scale")

        sub = col.column(align=True)

        sub.enabled = props.apply_unit_scale

        sub.prop(props, "scale_factor")

        col.prop(props, "apply_transforms")

        col.prop(props, "origin_to_geometry")


        layout.separator()


        layout.operator("import.sketchup_tag_dae_import", text="Import Tag DAEs", icon="IMPORT")


        box = layout.box()

        box.label(text="Result structure:")

        box.label(text="• SketchUp Importer")

        box.label(text="  - Extras")

        box.label(text="  - YYYY-MM-DD HH-MM (this import run)")



# ----------------------------

# Registration

# ----------------------------


classes = (

    SketchUpDAEImportProperties,

    SketchUpDAEImportOperator,

    SketchUpDAEImportPanel,

)


def register():

    for cls in classes:

        bpy.utils.register_class(cls)

    bpy.types.Scene.sketchup_dae_import_props = PointerProperty(type=SketchUpDAEImportProperties)


def unregister():

    for cls in reversed(classes):

        bpy.utils.unregister_class(cls)

    del bpy.types.Scene.sketchup_dae_import_props


Description

The SketchUp .dae Importer is a powerful add-on for Blender users working with .dae (Collada) files exported from SketchUp. It is specifically designed to complement a SketchUp Ruby export script that exports each tag as its own .dae file, enabling efficient batch import into Blender for further processing.

Key Features:

  1. Batch Import:The add-on allows users to select a folder containing multiple .dae files and import them all at once, eliminating the need for repetitive manual imports.

  2. Object Consolidation:For each .dae file, all geometry is automatically joined into a single object. This ensures a clean and organized Outliner while maintaining the name of the original .dae file for easy identification.

  3. Extras Handling:Non-geometry objects like curves and empties are moved into a dedicated collection named Extras. This keeps the workspace tidy and separates non-essential elements from the main geometry.

  4. Camera Removal:Any cameras included in the .dae files are automatically deleted during the import process, preventing unnecessary clutter.

  5. Collection Organization:All imported objects are grouped under a master collection called SketchUp Importer. This collection is created automatically if it doesn’t already exist, providing a centralized location for all imported files.

  6. Blender Compatibility:The add-on is compatible with Blender 4.2.0 and later, ensuring smooth integration with the latest features and improvements in Blender.

How It Works:

  1. Folder Selection:The user selects a directory containing .dae files via the add-on’s UI in Blender.

  2. Batch Import Process:
    The add-on loops through all .dae files in the selected folder and performs the following actions for each file:Imports the .dae file into Blender.
    Identifies all newly imported objects.

  3. Object Cleanup:
    The add-on processes imported objects as follows:Geometry: All imported geometry is joined into a single object and named after the original .dae file.
    Curves and Empties: Non-geometry objects are moved into the Extras collection.
    Cameras: Any imported cameras are deleted.

  4. Organizing Collections:The main geometry object is added to the SketchUp Importer collection.
    If the SketchUp Importer or Extras collections don’t exist, the add-on creates them automatically.

  5. Final Output:
    After processing all files, the Outliner contains a clean hierarchy:
    SketchUp Importer (master collection)Individual geometry objects (one per .dae file)
    Extras collection (containing curves and empties)

  6. Enhanced Workflow:The add-on ensures a streamlined pipeline, making it easier to manage large projects with multiple tags and layers exported from SketchUp.

Intended Use:

This add-on is tailored for users who rely on SketchUp’s Ruby export script to generate .dae files for each tag or layer. By automating the import and organization process in Blender, the add-on saves time, reduces errors, and improves efficiency for architects, designers, and 3D artists.

Use Case Example:

  1. In SketchUp, a user organizes their model into layers or tags (e.g., walls, doors, windows).

  2. Using the Ruby export script, they export each tag as a .dae file.

  3. In Blender, they use the SketchUp .dae Importer add-on to batch import these files, creating a structured and clean workspace with all objects consolidated and unnecessary elements removed.

Why It’s Useful:

  • Saves significant time for users importing multiple .dae files.

  • Ensures a clean, organized Outliner with minimal manual cleanup.

  • Complements SketchUp’s layer/tag system for efficient data transfer into Blender.

bottom of page