I would be great to have a function in the api to create a new file in a new session. Until that I think I will just keep the overwriting logic. Here is another version. Getting better and better. Now with some filters to rotate some parts.
Code:
function getSubfileDimensions(subfile)
local minVec = subfile:getBoundingBoxMin()
local maxVec = subfile:getBoundingBoxMax()
local width = maxVec:getX() - minVec:getX()
local height = maxVec:getY() - minVec:getY()
local depth = maxVec:getZ() - minVec:getZ()
return width, height, depth, minVec:getX(), minVec:getY(), minVec:getZ(), maxVec:getX(), maxVec:getY(),
maxVec:getZ()
end
function startsWith(str, start)
return string.sub(str, 1, string.len(start)) == start
end
function startsWithAny(str, substrings)
for _, substr in ipairs(substrings) do
if startsWith(str, substr) then
return true
end
end
return false
end
function containsAllWords(str, words)
for _, word in ipairs(words) do
if not string.find(str, word, 1, true) then -- Using plain search
return false
end
end
return true
end
function rotateToSmallestSideUpIfCircle(width, height, depth, minX, minY, minZ, maxX, maxY, maxZ, newRef)
local desc = newRef:getSubfile():getDescription()
local newRefPosOri = ldc.matrix()
local isRotated = false
if startsWithAny(desc, {"Wheel", "Tyre"}) or containsAllWords(desc, {"Technic", "Gear", "Tooth"}) or
containsAllWords(desc, {"Technic", "Wedge", "Belt", "Wheel"}) or startsWith(desc, "Technic Bush") or
startsWith(desc, "Technic Pulley") then
newRefPosOri:setRotate(90, ldc.vector(1, 0, 0))
newRef:setPosOri(newRefPosOri)
-- Swap depth and height
width, height, depth = width, depth, height
minX, minY, minZ, maxX, maxY, maxZ = minX, minZ, minY, maxX, maxZ, maxY
isRotated = true
elseif depth > width then
newRefPosOri:setRotate(90, ldc.vector(0, 1, 0))
newRef:setPosOri(newRefPosOri)
-- Swap depth and width
width, height, depth = depth, height, width
minX, minY, minZ, maxX, maxY, maxZ = minZ, minY, minX, maxZ, maxY, maxX
isRotated = true
end
return width, height, depth, minX, minY, minZ, maxX, maxY, maxZ, isRotated
end
function sortSubfiles(a, b, threshold)
local widthA, heightA, depthA, minXA, minYA, minZA, maxXA, maxYA, maxZA = getSubfileDimensions(a.sf)
local widthB, heightB, depthB, minXB, minYB, minZB, maxXB, maxYB, maxZB = getSubfileDimensions(b.sf)
-- Rotate to smallest side up
widthA, heightA, depthA, minXA, minYA, minZA, maxXA, maxYA, maxZA =
rotateToSmallestSideUpIfCircle(widthA, heightA, depthA, minXA, minYA, minZA, maxXA, maxYA, maxZA, a.ref)
widthB, depthB, heightB, minXB, minYB, minZB, maxXB, maxYB, maxZB =
rotateToSmallestSideUpIfCircle(widthB, heightB, depthB, minXB, minYB, minZB, maxXB, maxYB, maxZB, b.ref)
-- Determine largest sides after rotation
local largestA = math.max(widthA, heightA, depthA)
local largestB = math.max(widthB, heightB, depthB)
if largestA ~= largestB then
return largestA < largestB
end
-- Fallback to comparing by name if largest sides are equal or thresholds are not met
local nameA = a.sf:getFileName()
local nameB = b.sf:getFileName()
if nameA ~= nameB then
return nameA < nameB
end
-- Finally compare by color if names are also equal
return a.color < b.color
end
function buildFlatList(parentSf, parentPosOri, list)
local cnt = parentSf:getRefCount()
for i = 1, cnt do
local ref = parentSf:getRef(i)
local sf = ref:getSubfile()
local absPosOri = ref:getPosOri() * parentPosOri
if sf:isPart() or sf:isShortCut() then
table.insert(list, {
ref = ref,
color = ref:getColor(),
sf = sf
})
else
if sf:isGenerated() then
-- collect/map flex info if needed, for now just skip it.
-- print('flex: '..sf:getFileName())
else
buildFlatList(sf, absPosOri, list)
end
end
end
end
function deleteAllLines(mainSf)
for i = mainSf:getLineCount(), 1, -1 do
local line = mainSf:getLine(i)
line:delete()
end
end
function zeroOrValue(val1, val2)
local sum = val1 + val2
if sum == 0 then
return 0
else
return sum
end
end
function knollingLayout()
local ses = ldc.session()
if not ses:isLinked() then
ldc.dialog.runMessage('No active model.')
return
end
local maxRowSize = tonumber(ldc.dialog.runInput('Target width in LDU (minimum 40)?', '2000'))
if maxRowSize == nil or maxRowSize < 40 then
return
end
local layoutDirection = ldc.dialog.runInput('Layout direction (width or depth)', 'depth')
if layoutDirection == nil or (layoutDirection ~= 'depth' and layoutDirection ~= 'width') then
return
end
local list = {}
local mainSf = ldc.subfile()
buildFlatList(mainSf, ldc.matrix(), list)
local threshold = 50 -- Set this to your desired threshold value
table.sort(list, function(a, b)
return sortSubfiles(a, b, threshold)
end)
deleteAllLines(mainSf)
local spacing = 20 -- Adjust spacing if needed
local currentRowSize = 0
local currentRowMaxDim = 0
local mainOffset = 0
local secondaryOffset = 0
local newRefs = {}
local pivotOffsetZ = 0
for _, item in ipairs(list) do
local isRotated = false
local newRef = mainSf:addNewRef(item.sf:getFileName())
newRef:setColor(item.color)
local width, height, depth, minX, minY, minZ, maxX, maxY, maxZ = getSubfileDimensions(item.sf)
-- Rotate to smallest side up if the other two sides are equal and get the new center coordinates
width, height, depth, minX, minY, minZ, maxX, maxY, maxZ, isRotated =
rotateToSmallestSideUpIfCircle(width, height, depth, minX, minY, minZ, maxX, maxY, maxZ, newRef)
local primarySize, secondarySize, primaryMin, secondaryMin
if layoutDirection == 'width' then
primarySize = width
secondarySize = depth
primaryMin = minX
secondaryMin = minZ
else
primarySize = depth
secondarySize = width
primaryMin = minZ
secondaryMin = minX
end
if currentRowSize + primarySize + spacing > maxRowSize then
mainOffset = 0
secondaryOffset = secondaryOffset + currentRowMaxDim + spacing
currentRowSize = 0
currentRowMaxDim = 0
lastMax = 0
end
local yPos = 0
if isRotated and maxY ~= 0 then
yPos = math.min(minY, -maxY)
else
yPos = -maxY
end
local pos
if layoutDirection == 'width' then
pos = ldc.vector(mainOffset - primaryMin + spacing, yPos, secondaryOffset - secondaryMin + spacing)
else
pos = ldc.vector(secondaryOffset - secondaryMin + spacing, yPos, mainOffset - primaryMin + spacing)
end
newRef:setPos(pos)
mainOffset = mainOffset + primarySize + spacing
currentRowSize = currentRowSize + primarySize + spacing
currentRowMaxDim = math.max(currentRowMaxDim, secondarySize + spacing)
table.insert(newRefs, newRef)
end
end
-- Register the macro for the knolling layout
function register()
local macro_lo = ldc.macro('Perform knolling layout')
macro_lo:setHint('Arrange elements densely with even spacing in the XY plane with Z = 0.')
macro_lo:setEvent('run', 'knollingLayout')
end
register()