Share

Script48_TrueShapePack.lua

-- Lua example for Autodesk Netfabb 2025.0

-- Demonstrates UI and packing using True Shape

function RotationTypeChange()
    local Item = RotationType.selecteditem

    RotationStepX.enabled = Item == 0
    RotationStepY.enabled = Item == 0
    RotationStepZ.enabled = Item == 0
    RotationMultiAxis.enabled = Item == 0
    RotationList.enabled = Item == 1
end

function ComponentPlacementChanged()
    InterlockCheck.enabled = AdvancedSettings.checked
    CompPlacement.enabled = AdvancedSettings.checked
    Priorities.enabled = AdvancedSettings.checked

    Axis.enabled = AdvancedSettings.checked and CompPlacement.selecteditem == 0
    SweetSpotX.enabled = AdvancedSettings.checked and CompPlacement.selecteditem == 1
    SweetSpotY.enabled = AdvancedSettings.checked and CompPlacement.selecteditem == 1
end

function CloneParent(Parent, Target, Count)
    local SourceMeshes = {}

    for Index = 0, Parent.meshcount - 1 do
        table.insert(SourceMeshes, Parent:getmesh(Index))
    end

    for k, v in pairs(SourceMeshes) do
        for Clone = 1, Count do
            if not v.lockedposition then
                local Mesh = Target:addmesh(v.mesh)
                Mesh.name = v.name .. "_" .. tostring(Clone)
                Mesh:setmatrix(v.matrix)
            end
        end
    end

    for Index = 0, Parent.groupcount - 1 do
        for Clone = 1, Count do
            local Group = Parent:getsubgroup(Index)
            local NewGroup = Target:addsubgroup(Group.name .. "_" .. tostring(Clone))

            NewGroup.locked = Group.locked

            CloneParent(Group, NewGroup, 1)
        end
    end
end

-- Move the parts of the group outside the platform,
-- otherwise parts which cannot be packed are not moved at all
function MovePartsOutsidePlatform(Group)
    for Index = 0, Group.meshcount - 1 do
        local Mesh = Group:getmesh(Index)

        if not Mesh.lockedposition then
            local BoundingBox = Mesh.outbox

            Mesh:translate(-(BoundingBox.maxx + BoundingBox.sizex), -(BoundingBox.maxy + BoundingBox.sizey), 0)
        end
    end

    for Index = 0, Group.groupcount - 1 do
        local SubGroup = Group:getsubgroup(Index)
        local BoundingBox = Group.outbox

        SubGroup:translate(-(BoundingBox.maxx + BoundingBox.sizex), -(BoundingBox.maxy + BoundingBox.sizey), 0)

        system:messagedlg(
            "Move group: " ..
                tostring(-(BoundingBox.maxx + BoundingBox.sizex)) ..
                    ", " .. tostring(-(BoundingBox.maxy + BoundingBox.sizey))
        )
    end
end

function Change2dPacking()
    UseShadow.enabled = TwoDPacking.checked

    Axis:clear()
    Axis:additem("Positive X axis", 0, 0, false)
    Axis:additem("Positive Y axis", 1, 1, false)

    if not TwoDPacking.checked then
        Axis:additem("Positive Z Axis", 2, 2, false)
    end

    Axis:additem("Negative X Axis", 3, 3, false)
    Axis:additem("Negative Y Axis", 4, 4, false)

    if not TwoDPacking.checked then
        Axis:additem("Negative Z Axis", 5, 5, false)
    end

    Axis:updateitems()
end

function BtnPack()
    Dialog:close(true)
end

function BtnClose()
    Dialog:close(false)
end

system:setloggingtooglwindow(true)

CaptionWidth = 180

Dialog = application:createdialog()

Dialog.caption = "Lua automation demo for True Shape packing"
Dialog.translatecaption = false
Dialog.width = 360

Group = Dialog:addgroupbox()

Group.caption = "Settings"
Group.translate = false

TwoDPacking = Group:addcheckbox()
TwoDPacking.caption = "2d packing"
TwoDPacking.translate = false
TwoDPacking.onclick = "Change2dPacking"

UseShadow = Group:addcheckbox()
UseShadow.caption = "Avoid packing parts in the shadow of other parts"
UseShadow.translate = false
UseShadow.enabled = false

PackingOrder = Group:adddropdown()

PackingOrder.caption = "Packing order"
PackingOrder.captionwidth = CaptionWidth
PackingOrder.translatecaption = false
PackingOrder.translate = false

PackingOrder:additem("Part volume or priority", 0, 0, false)
PackingOrder:additem("Project tree", 1, 1, false)

VoxelSize = Group:addedit()
VoxelSize.caption = "Voxel size"
VoxelSize.translate = false
VoxelSize.captionwidth = CaptionWidth
VoxelSize.text = "4.0"
VoxelSize.numbersonly = true

PartCopies = Group:addedit()
PartCopies.caption = "Global part duplicates"
PartCopies.translate = false
PartCopies.captionwidth = CaptionWidth
PartCopies.text = "1"
PartCopies.numbersonly = true

PartDistance = Group:addedit()
PartDistance.caption = "Minimum part distance (mm)"
PartDistance.translate = false
PartDistance.captionwidth = CaptionWidth
PartDistance.text = "2.0"
PartDistance.numbersonly = true

DistanceXY = Group:addedit()
DistanceXY.caption = "Distance to side walls (XY, mm)"
DistanceXY.translate = false
DistanceXY.captionwidth = CaptionWidth
DistanceXY.text = "2.0"
DistanceXY.numbersonly = true

DistancePlatform = Group:addedit()
DistancePlatform.caption = "Distance to platform (mm)"
DistancePlatform.translate = false
DistancePlatform.captionwidth = CaptionWidth
DistancePlatform.text = "2.0"
DistancePlatform.numbersonly = true

DistanceCeiling = Group:addedit()
DistanceCeiling.caption = "Distance to ceiling (mm)"
DistanceCeiling.translate = false
DistanceCeiling.captionwidth = CaptionWidth
DistanceCeiling.text = "2.0"
DistanceCeiling.numbersonly = true

RotationType = Group:adddropdown()
RotationType.caption = "Rotation type"
RotationType.captionwidth = CaptionWidth
RotationType.translatecaption = false
RotationType.translate = false
RotationType.onchange = "RotationTypeChange"

RotationType:additem("Rotation steppings", 0, 0, false)
RotationType:additem("Rotations list", 1, 1, false)

RotationStepX = Group:adddropdown()
RotationStepX.caption = "X rotation"
RotationStepX.captionwidth = CaptionWidth
RotationStepX.translatecaption = false
RotationStepX.translate = false

RotationStepX:additem("0.0", 0, 0, false)
RotationStepX:additem("180.0", 1, 1, false)
RotationStepX:additem("90.0", 2, 2, false)
RotationStepX:additem("60.0", 3, 3, false)
RotationStepX:additem("45.0", 4, 4, false)
RotationStepX:additem("36.0", 5, 5, false)
RotationStepX:additem("30.0", 6, 6, false)
RotationStepX:additem("20.0", 7, 7, false)

RotationStepY = Group:adddropdown()
RotationStepY.caption = "Y rotation"
RotationStepY.captionwidth = CaptionWidth
RotationStepY.translatecaption = false
RotationStepY.translate = false

RotationStepY:additem("0.0", 0, 0, false)
RotationStepY:additem("180.0", 1, 1, false)
RotationStepY:additem("90.0", 2, 2, false)
RotationStepY:additem("60.0", 3, 3, false)
RotationStepY:additem("45.0", 4, 4, false)
RotationStepY:additem("36.0", 5, 5, false)
RotationStepY:additem("30.0", 6, 6, false)
RotationStepY:additem("20.0", 7, 7, false)

RotationStepZ = Group:adddropdown()
RotationStepZ.caption = "Z rotation"
RotationStepZ.captionwidth = CaptionWidth
RotationStepZ.translatecaption = false
RotationStepZ.translate = false

RotationStepZ:additem("0.0", 0, 0, false)
RotationStepZ:additem("180.0", 1, 1, false)
RotationStepZ:additem("90.0", 2, 2, false)
RotationStepZ:additem("60.0", 3, 3, false)
RotationStepZ:additem("45.0", 4, 4, false)
RotationStepZ:additem("36.0", 5, 5, false)
RotationStepZ:additem("30.0", 6, 6, false)
RotationStepZ:additem("20.0", 7, 7, false)

RotationStepZ.selecteditem = 2

RotationMultiAxis = Group:addcheckbox()
RotationMultiAxis.caption = "Include mulit-axis variants"
RotationMultiAxis.translate = false

RotationList = Group:addedit()
RotationList.caption = "Rotations list (X;Y;Z)"
RotationList.translate = false
RotationList.captionwidth = CaptionWidth
RotationList.text = "(0;0;0)(0;0;90)(0;0;180)(0;0;270)"
RotationList.enabled = false

AdvancedSettings = Group:addcheckbox()
AdvancedSettings.caption = "Advanced packing settings"
AdvancedSettings.translate = false
AdvancedSettings.checked = false
AdvancedSettings.onclick = "ComponentPlacementChanged"

InterlockCheck = Group:addcheckbox()
InterlockCheck.caption = "Interlock check"
InterlockCheck.translate = false
InterlockCheck.checked = true

CompPlacement = Group:adddropdown()
CompPlacement.caption = "Part placement"
CompPlacement.captionwidth = CaptionWidth
CompPlacement.translatecaption = false
CompPlacement.translate = false
CompPlacement.onchange = "ComponentPlacementChanged"

CompPlacement:additem("Along axis", 0, 0, false)
CompPlacement:additem("Sweet spot", 1, 1, false)
CompPlacement:additem("Center bias", 2, 2, false)

Axis = Group:adddropdown()
Axis.caption = "Direction"
Axis.captionwidth = CaptionWidth
Axis.translatecaption = false
Axis.translate = false

Axis:additem("Positive X Axis", 0, 0, false)
Axis:additem("Positive Y Axis", 1, 1, false)
Axis:additem("Positive Z Axis", 2, 2, false)

Axis:additem("Negative X Axis", 3, 3, false)
Axis:additem("Negative Y Axis", 4, 4, false)
Axis:additem("Negative Z Axis", 5, 5, false)

SweetSpotX = Group:addedit()
SweetSpotX.caption = "Sweet spot X position"
SweetSpotX.translate = false
SweetSpotX.captionwidth = CaptionWidth
SweetSpotX.text = "0.0"
SweetSpotX.numbersonly = true

SweetSpotY = Group:addedit()
SweetSpotY.caption = "Sweet spot Y position"
SweetSpotY.translate = false
SweetSpotY.captionwidth = CaptionWidth
SweetSpotY.text = "0.0"
SweetSpotY.numbersonly = true

Priorities = Group:adddropdown()
Priorities.caption = "Placement priorities"
Priorities.captionwidth = CaptionWidth
Priorities.translatecaption = false
Priorities.translate = false

Priorities:additem("Example 1", 0, 0, false)
Priorities:additem("Example 2", 1, 1, false)
Priorities:additem("Example 3", 2, 2, false)
Priorities:additem("Example 4", 3, 3, false)
Priorities:additem("All Options", 4, 4, false)

Splitter = Dialog:addsplitter()
Splitter:settoleft()

ButtonPack = Splitter:addbutton()
ButtonPack.caption = "Pack"
ButtonPack.translate = false
ButtonPack.onclick = "BtnPack"

Splitter:settoright()

ButtonClose = Splitter:addbutton()
ButtonClose.caption = "Close"
ButtonClose.translate = false
ButtonClose.onclick = "BtnClose"

ComponentPlacementChanged()

if Dialog:show() == true then
    local Copies = tonumber(PartCopies.text)

    if Copies > 1 then
        CloneParent(tray.root, tray.root, Copies - 1)
    end

    local Done = false
    local Tray = tray

    repeat
        MovePartsOutsidePlatform(Tray.root)

        local Packer = Tray:createpacker(Tray.packingid_trueshape)

        Packer.showprogress = true
        Packer.packing_2d = TwoDPacking.checked
        Packer.packing_use_shadow_2d = UseShadow.checked
        Packer.rotation_use_compound = RotationMultiAxis.checked
        Packer.rotation_use_list = RotationType.selecteditem == 1
        Packer.borderspacingxy = tonumber(DistanceXY.text)
        Packer.borderspacingz = tonumber(DistancePlatform.text)
        Packer.minimaldistance = tonumber(PartDistance.text)
        Packer.voxel_size = tonumber(VoxelSize.text)
        Packer.rotation_x = tonumber(RotationStepX:getitemtext(RotationStepX.selecteditem))
        Packer.rotation_y = tonumber(RotationStepY:getitemtext(RotationStepY.selecteditem))
        Packer.rotation_z = tonumber(RotationStepZ:getitemtext(RotationStepZ.selecteditem))
        Packer.rotation_list = RotationList.text

        -- If wanted we apply the advanced settings
        if AdvancedSettings.checked == true then
            Packer.avoid_interlocking = InterlockCheck.checked

            local Placement = CompPlacement.selecteditem

            if Placement == 0 then
                Packer.part_placement = Packer.place_alongaxis

                if TwoDPacking.checked then
                    -- For 2D packing only two values are passed to "setdirectionaxis"
                    -- using packing constants "axis_[positive|negative]_[x|y|z]
                    if Axis.selecteditem == 0 then
                        Packer:setdirectionaxis(Packer.axis_positive_x, Packer.axis_positive_y)
                    elseif Axis.selecteditem == 1 then
                        Packer:setdirectionaxis(Packer.axis_positive_y, Packer.axis_positive_x)
                    elseif Axis.selecteditem == 3 then
                        Packer:setdirectionaxis(Packer.axis_negative_x, Packer.axis_negative_y)
                    elseif Axis.selecteditem == 4 then
                        Packer:setdirectionaxis(Packer.axis_negative_y, Packer.axis_negative_x)
                    end
                else
                    -- Setting the axes for 3d packing needs three values passed
                    -- to the "setdirectionaxis" method, using the packing constants
                    -- "axis_[positive|negative]_[x|y|z]
                    if Axis.selecteditem == 0 then
                        Packer:setdirectionaxis(Packer.axis_positive_x, Packer.axis_positive_y, Packer.axis_positive_z)
                    elseif Axis.selecteditem == 1 then
                        Packer:setdirectionaxis(Packer.axis_positive_y, Packer.axis_positive_x, Packer.axis_positive_z)
                    elseif Axis.selecteditem == 2 then
                        Packer:setdirectionaxis(Packer.axis_positive_z, Packer.axis_positive_x, Packer.axis_positive_y)
                    elseif Axis.selecteditem == 3 then
                        Packer:setdirectionaxis(Packer.axis_negative_x, Packer.axis_negative_y, Packer.axis_positive_z)
                    elseif Axis.selecteditem == 4 then
                        Packer:setdirectionaxis(Packer.axis_negative_y, Packer.axis_negative_x, Packer.axis_positive_z)
                    elseif Axis.selecteditem == 5 then
                        Packer:setdirectionaxis(Packer.axis_negative_z, Packer.axis_negative_x, Packer.axis_negative_y)
                    end
                end
            elseif Placement == 1 then
                Packer.part_placement = Packer.place_sweetspot
                Packer.sweetspot_x = tonumber(SweetSpotX.text)
                Packer.sweetspot_y = tonumber(SweetSpotY.text)
            elseif Placement == 2 then
                -- The "Center bias" option puts the sweet spot
                -- at the center of the platform
                Packer.part_placement = Packer.place_sweetspot
                Packer.sweetspot_x = Tray.machinesize_x / 2.0
                Packer.sweetspot_y = Tray.machinesize_x / 2.0
            end

            -- Here we pass some example values for priority ranking.
            -- The meshes are sorted using the first criterion, if no
            -- distinction was detected the second is used, and so on.
            -- All possible options are used in the four examples. Here
            -- we only pass 3 options but all 6 can be used as well.
            -- Example 5 shows that
            if Priorities.selecteditem == 0 then
                Packer:setplacementpriorities(
                    Packer.minimum_buildbox_volume,
                    Packer.minimum_build_height,
                    Packer.maximum_contact_area
                )
            elseif Priorities.selecteditem == 1 then
                Packer:setplacementpriorities(
                    Packer.minimum_build_height,
                    Packer.maximum_contact_area,
                    Packer.maximum_box_overlap
                )
            elseif Priorities.selecteditem == 2 then
                Packer:setplacementpriorities(
                    Packer.maximum_contact_area,
                    Packer.maximum_box_overlap,
                    Packer.minimum_part_box_volume
                )
            elseif Priorities.selecteditem == 3 then
                Packer:setplacementpriorities(
                    Packer.maximum_box_overlap,
                    Packer.minimum_part_box_volume,
                    Packer.minimum_part_height
                )
            elseif Priorities.selecteditem == 4 then
                Packer:setplacementpriorities(
                    Packer.minimum_buildbox_volume,
                    Packer.minimum_build_height,
                    Packer.maximum_contact_area,
                    Packer.maximum_box_overlap,
                    Packer.minimum_part_box_volume,
                    Packer.minimum_part_height
                )
            end
        end

        -- set some of the additional parameters if the null packer is used
        local errorcode = Packer:pack()

        local UnPackedList = {}
        local UnPackedCount = 0
        local PackedCount = 0

        for Index = 0, Tray.root.meshcount - 1 do
            local Mesh = Tray.root:getmesh(Index)
            if Mesh.packingstate == Packer.isLeftover then
                UnPackedCount = UnPackedCount + 1
                table.insert(UnPackedList, Mesh)
            elseif tray.root:getmesh(Index).packingstate == Packer.isPacked then
                PackedCount = PackedCount + 1
            end
        end

        if UnPackedCount == 0 then
            system:messagedlg("All parts were packed, packing finished.")
            Done = true
        else
            local Input =
                system:yesnodlg(
                tostring(UnPackedCount) .. " parts were not packed. Do you want to continue on a new platform?"
            )
            if Input == 0 then
                Done = true
            else
                local NewTray =
                    netfabbtrayhandler:addtray(
                    "NewPlatform_" .. tostring(netfabbtrayhandler.traycount),
                    Tray.machinesize_x,
                    Tray.machinesize_y,
                    Tray.machinesize_z
                )

                for k, v in pairs(UnPackedList) do
                    NewTray.root:addmesh(v.mesh)
                    Tray.root:removemesh(v)
                end

                Tray = NewTray
            end
        end
    until Done == true
end

Was this information helpful?