CadPaste

SolidWorks

VBA-макросы для SolidWorks.

Нажмите /, чтобы перейти к поиску.

SolidWorks · VBA · 2 дня назад · обновлено сегодня

Расчет цены листовых материалов

Макрос для расчета цены листовых материалов в зависимости от размера заготовки, длины реза, количества гибов.

VBA
'==============================================================================
' SheetMetalCalc.bas — SolidWorks VBA macro
' Mirrors "Preliminary Cost Estimate" panel (Solidworks panel add-in).
'
' WHAT THIS MACRO DOES
'   For the active sheet-metal Part:
'     - reads part / thickness / material / weight
'     - temporarily flattens FlatPattern
'     - measures area, blank (+ allowance), laser path, bend count / length
'     - checks flat-body geometry (Body.Check2)
'     - computes material / cut / bend / engraver / total price
'   Shows the same text report as the panel (MsgBox) and optionally saves .txt
'   next to the model: {name}_{material}_{thickness}_{yyyy-MM-dd_HH-mm-ss}.txt
'
' HOW TO USE
'   1) Edit SETTINGS below (prices, allowance, export flags).
'   2) Open a saved sheet-metal Part in SolidWorks.
'   3) Tools > Macro > Run > select this .bas (entry point: main).
'
' NOTES
'   - Late binding (Object) — no SolidWorks type library required.
'   - FlatPattern is restored to the previous suppressed/unsuppressed state.
'   - Settings mirror CostSettingsData defaults from the panel add-in.
'==============================================================================

Option Explicit

'----- SETTINGS (edit these — same meaning as panel Cost Settings) ------------
' Values below match a typical panel preset; dump of these is appended to report
' under "Settings" so a .txt export can restore the calculation inputs later.

Private Const iDecimalPlaces As Integer = 2
' Places after decimal for geometry values (price always 2). Panel: Decimal places.

Private Const dMaterialAllowanceMm As Double = 1#
' Blank allowance per side, mm. Panel: Material allowance (mm).

Private Const dPricePerBend As Double = 2#
' Price for one bend. Panel: Price per 1 bend.

Private Const dPricePerMmBendLineEngraver As Double = 0.1
' Price per 1 mm of bend-line engraver path. Panel: Price per 1 mm bend line.

Private Const dPricePerMmCutPer1MmThickness As Double = 13.55
' Cut price per 1 mm path at 1 mm thickness. Panel: Price per 1 mm cut.

Private Const dMaterialPrice As Double = 12#
' Material unit price. Panel: Material price.

Private Const bMaterialPricePerKg As Boolean = True
' True = Per 1 kg; False = Per 1 m?. Panel: Material price unit.

Private Const bMaterialNet As Boolean = False
' True = Area (Net); False = Blank (Gross). Panel: Material quantity.

Private Const bExportTxt As Boolean = True
' True = write report .txt next to the part file.

Private Const iNotify As Integer = 1
' 0 = MsgBox only on failure; 1 = always show report MsgBox.

'----- SW API constants -------------------------------------------------------
Private Const swDocPART As Long = 1
Private Const swUnSuppressFeature As Long = 2
Private Const swSuppressFeature As Long = 0
Private Const swThisConfiguration As Long = 1
Private Const swSketchLINE As Long = 0
Private Const API_TO_MM As Double = 1000#

Private Const KEY_COL As Integer = 12
Private Const SETTINGS_KEY_COL As Integer = 24
Private Const MODULE_SEP As String = "---"
Private Const UNDEF As String = "undefined"

'----- Module state -----------------------------------------------------------
Private swApp As Object
Private swModel As Object

'==============================================================================
' ENTRY
'==============================================================================
Public Sub main()
    Dim report As String
    Dim path As String
    Dim ok As Boolean

    On Error GoTo Fatal
    Set swApp = Application.SldWorks
    If swApp Is Nothing Then Set swApp = GetObject(, "SldWorks.Application")
    If swApp Is Nothing Then
        MsgBox "SolidWorks is not running.", vbExclamation, "SheetMetalCalc"
        Exit Sub
    End If

    Set swModel = swApp.ActiveDoc
    If swModel Is Nothing Then
        MsgBox "No document open.", vbExclamation, "SheetMetalCalc"
        Exit Sub
    End If

    report = BuildReport()
    ok = (Len(report) > 0) And (InStr(1, report, "No document", vbTextCompare) = 0)

    If bExportTxt And ok And CanWriteNextToModel() Then
        path = BuildTxtPath()
        If Len(path) > 0 Then
            If WriteTextFile(path, report) Then
                report = report & vbCrLf & MODULE_SEP & vbCrLf & _
                         PadKey("export") & " - " & path
            Else
                report = report & vbCrLf & MODULE_SEP & vbCrLf & _
                         "Export failed: could not write TXT."
                ok = False
            End If
        End If
    End If

    If iNotify = 1 Or Not ok Then
        MsgBox report, IIf(ok, vbInformation, vbExclamation), "SheetMetalCalc"
    End If
    Exit Sub

Fatal:
    MsgBox "SheetMetalCalc error: " & Err.Description, vbCritical, "SheetMetalCalc"
End Sub

'==============================================================================
' REPORT (panel-compatible layout)
'==============================================================================
Private Function BuildReport() As String
    Dim modules As String
    Dim infoPart As String, infoThk As String, infoMat As String, infoW As String
    Dim isSm As Boolean
    Dim canMsg As String
    Dim areaMm2 As Double, blankL As Double, blankW As Double, blankA As Double
    Dim laserMm As Double, bendLenMm As Double
    Dim bendCnt As Long
    Dim hasArea As Boolean, hasBlank As Boolean, hasLaser As Boolean
    Dim hasBendCnt As Boolean, hasBendLen As Boolean
    Dim hasOverlap As Boolean, overlapMsg As String
    Dim weightKg As Double, thkMm As Double
    Dim hasWeight As Boolean, hasThk As Boolean
    Dim matCost As Variant, cutCost As Variant, bendCost As Variant, engCost As Variant
    Dim totalTxt As String
    Dim sheetBlock As String
    Dim engBlock As String

    GetPartInfo isSm, infoPart, infoThk, infoMat, infoW, hasWeight, weightKg, hasThk, thkMm

    If Not isSm Then
        BuildReport = "Document is not a sheet part." & vbCrLf & _
                      "(Open a part with Flat Pattern / sheet metal.)" & _
                      vbCrLf & MODULE_SEP & vbCrLf & BuildSettingsBlock()
        Exit Function
    End If

    If Not CanFlatten(canMsg) Then
        BuildReport = PadKey("part") & " - " & infoPart & " loaded" & vbCrLf & _
                      PadKey("thickness") & " - " & infoThk & vbCrLf & _
                      PadKey("material") & " - " & infoMat & vbCrLf & _
                      IIf(hasWeight, PadKey("weight") & " - " & infoW & vbCrLf, "") & _
                      PadKey("price") & " - (not calculated)" & vbCrLf & _
                      MODULE_SEP & vbCrLf & canMsg & _
                      vbCrLf & MODULE_SEP & vbCrLf & BuildSettingsBlock()
        Exit Function
    End If

    If Not CalcOnFlatPattern(areaMm2, hasArea, blankL, blankW, blankA, hasBlank, _
                             laserMm, hasLaser, bendCnt, hasBendCnt, bendLenMm, hasBendLen, _
                             hasOverlap, overlapMsg) Then
        BuildReport = PadKey("part") & " - " & infoPart & " loaded" & vbCrLf & _
                      PadKey("thickness") & " - " & infoThk & vbCrLf & _
                      PadKey("material") & " - " & infoMat & vbCrLf & _
                      IIf(hasWeight, PadKey("weight") & " - " & infoW & vbCrLf, "") & _
                      PadKey("price") & " - (not calculated)" & vbCrLf & _
                      MODULE_SEP & vbCrLf & _
                      "Cannot flatten this part: Flat-Pattern/Flatten operation is unavailable." & _
                      vbCrLf & MODULE_SEP & vbCrLf & BuildSettingsBlock()
        Exit Function
    End If

    If hasOverlap Then
        BuildReport = PadKey("part") & " - " & infoPart & " loaded" & vbCrLf & _
                      PadKey("thickness") & " - " & infoThk & vbCrLf & _
                      PadKey("material") & " - " & infoMat & vbCrLf & _
                      IIf(hasWeight, PadKey("weight") & " - " & infoW & vbCrLf, "") & _
                      PadKey("price") & " - (not calculated)" & vbCrLf & _
                      MODULE_SEP & vbCrLf & _
                      IIf(Len(overlapMsg) > 0, overlapMsg, "Geometry check found issues in flat pattern.") & _
                      vbCrLf & MODULE_SEP & vbCrLf & BuildSettingsBlock()
        Exit Function
    End If

    If hasBendCnt And bendCnt <= 0 Then
        bendLenMm = 0#
        hasBendLen = True
    End If

    matCost = CalcMaterialCost(hasWeight, weightKg, hasArea, areaMm2, hasBlank, blankA, hasThk, thkMm)
    cutCost = CalcCutCost(hasLaser, laserMm, hasThk, thkMm)
    bendCost = CalcBendCost(hasBendCnt, bendCnt)
    engCost = CalcEngraverCost(hasBendLen, bendLenMm)
    totalTxt = FormatTotalPrice(matCost, cutCost, bendCost, engCost)

    ' Module 1 — part summary
    modules = PadKey("part") & " - " & infoPart & " loaded" & vbCrLf & _
              PadKey("thickness") & " - " & infoThk & vbCrLf & _
              PadKey("material") & " - " & infoMat & vbCrLf
    If hasWeight Then modules = modules & PadKey("weight") & " - " & infoW & vbCrLf
    modules = modules & PadKey("price") & " - " & totalTxt

    ' Module 2 — sheet / laser / bends
    sheetBlock = "Sheet OK" & vbCrLf
    If hasArea Then
        sheetBlock = sheetBlock & PadKey("area") & " - " & FmtNum(areaMm2, iDecimalPlaces) & " mm?" & vbCrLf
    End If
    If hasBlank Then
        sheetBlock = sheetBlock & PadKey("blank") & " - " & _
                     FmtNum(blankL, iDecimalPlaces) & " x " & FmtNum(blankW, iDecimalPlaces) & " mm (" & _
                     FmtNum(blankA, iDecimalPlaces) & " mm?), allowance +" & _
                     FmtNumG(dMaterialAllowanceMm) & " mm per side (from settings)" & vbCrLf
    End If
    sheetBlock = sheetBlock & PadKey("material price") & " - " & FormatMoney(matCost) & vbCrLf
    sheetBlock = sheetBlock & MODULE_SEP & vbCrLf & "Laser cut" & vbCrLf
    If hasLaser Then
        sheetBlock = sheetBlock & PadKey("length") & " - " & FmtNum(laserMm, iDecimalPlaces) & " mm" & vbCrLf
        sheetBlock = sheetBlock & PadKey("cutting price") & " - " & FormatMoney(cutCost) & vbCrLf
    Else
        sheetBlock = sheetBlock & PadKey("length") & " - —" & vbCrLf
        sheetBlock = sheetBlock & PadKey("cutting price") & " - (not calculated)" & vbCrLf
    End If
    sheetBlock = sheetBlock & MODULE_SEP & vbCrLf & "Bends" & vbCrLf
    sheetBlock = sheetBlock & PadKey("count") & " - " & CStr(IIf(hasBendCnt, bendCnt, 0)) & vbCrLf
    sheetBlock = sheetBlock & PadKey("bending price") & " - " & FormatMoney(bendCost)

    modules = modules & vbCrLf & MODULE_SEP & vbCrLf & sheetBlock

    ' Module 3 — engraver
    engBlock = "Engraver" & vbCrLf
    If hasBendLen Then
        engBlock = engBlock & PadKey("length") & " - " & FmtNum(bendLenMm, iDecimalPlaces) & " mm" & vbCrLf
    Else
        engBlock = engBlock & PadKey("length") & " - —" & vbCrLf
    End If
    engBlock = engBlock & PadKey("rate") & " - " & FmtNumG(dPricePerMmBendLineEngraver) & vbCrLf
    engBlock = engBlock & PadKey("price") & " - " & FormatMoney(engCost)
    modules = modules & vbCrLf & MODULE_SEP & vbCrLf & engBlock

    ' Module 4 — settings snapshot (for restore from TXT)
    modules = modules & vbCrLf & MODULE_SEP & vbCrLf & BuildSettingsBlock()

    BuildReport = modules
End Function

Private Function BuildSettingsBlock() As String
    Dim s As String
    s = "Settings" & vbCrLf
    s = s & PadSettingsKey("decimal places") & " - " & CStr(ClampDec(iDecimalPlaces)) & vbCrLf
    s = s & PadSettingsKey("material allowance") & " - " & FmtNumG(dMaterialAllowanceMm) & " mm" & vbCrLf
    s = s & PadSettingsKey("price per bend") & " - " & FmtNumG(dPricePerBend) & vbCrLf
    s = s & PadSettingsKey("price per mm engraver") & " - " & FmtNumG(dPricePerMmBendLineEngraver) & vbCrLf
    s = s & PadSettingsKey("price per mm cut") & " - " & FmtNumG(dPricePerMmCutPer1MmThickness) & vbCrLf
    s = s & PadSettingsKey("material price") & " - " & FmtNumG(dMaterialPrice) & vbCrLf
    s = s & PadSettingsKey("material price unit") & " - " & IIf(bMaterialPricePerKg, "Per 1 kg", "Per 1 m2") & vbCrLf
    s = s & PadSettingsKey("material quantity") & " - " & IIf(bMaterialNet, "Area", "Blank")
    BuildSettingsBlock = s
End Function

Private Function PadKey(ByVal key As String) As String
    If Len(key) >= KEY_COL Then
        PadKey = key
    Else
        PadKey = key & Space$(KEY_COL - Len(key))
    End If
End Function

Private Function PadSettingsKey(ByVal key As String) As String
    If Len(key) >= SETTINGS_KEY_COL Then
        PadSettingsKey = key
    Else
        PadSettingsKey = key & Space$(SETTINGS_KEY_COL - Len(key))
    End If
End Function

' Invariant decimal point (panel uses "."); avoids locale commas in MsgBox/TXT.
Private Function FmtNum(ByVal v As Double, ByVal dec As Integer) As String
    Dim n As Integer
    Dim s As String
    n = ClampDec(dec)
    If n = 0 Then
        s = Format$(v, "0")
    Else
        s = Format$(v, "0." & String$(n, "0"))
    End If
    FmtNum = Replace(Replace(s, " ", ""), ",", ".")
End Function

Private Function FmtNumG(ByVal v As Double) As String
    Dim s As String
    s = Trim$(Str$(v)) ' Str always uses "." as decimal separator
    FmtNumG = s
End Function

Private Function FormatMoney(ByVal v As Variant) As String
    If IsEmpty(v) Or IsNull(v) Then
        FormatMoney = "(not calculated)"
    Else
        FormatMoney = FmtNum(CDbl(v), 2)
    End If
End Function

Private Function ClampDec(ByVal n As Integer) As Integer
    If n < 0 Then
        ClampDec = 0
    ElseIf n > 10 Then
        ClampDec = 10
    Else
        ClampDec = n
    End If
End Function

'==============================================================================
' PART INFO
'==============================================================================
Private Sub GetPartInfo(ByRef isSm As Boolean, ByRef partName As String, _
                        ByRef thickness As String, ByRef material As String, ByRef weight As String, _
                        ByRef hasWeight As Boolean, ByRef weightKg As Double, _
                        ByRef hasThk As Boolean, ByRef thkMm As Double)
    Dim feat As Object
    Dim smData As Object
    Dim hasFlat As Boolean
    Dim thkM As Double
    Dim hasThkM As Boolean
    Dim database As String
    Dim matName As String
    Dim ext As Object
    Dim massProp As Object
    Dim mass As Double

    isSm = False
    partName = UNDEF
    thickness = UNDEF
    material = UNDEF
    weight = UNDEF
    hasWeight = False
    weightKg = 0#
    hasThk = False
    thkMm = 0#
    hasFlat = False
    hasThkM = False

    On Error Resume Next
    partName = swModel.GetTitle
    If Len(partName) = 0 Then partName = UNDEF

    If swModel.GetType <> swDocPART Then Exit Sub

    Set feat = swModel.FirstFeature
    Do While Not feat Is Nothing
        If StrComp(feat.GetTypeName2, "FlatPattern", vbTextCompare) = 0 Then hasFlat = True
        If StrComp(feat.GetTypeName2, "SheetMetal", vbTextCompare) = 0 Then
            Set smData = feat.GetDefinition
            If Not smData Is Nothing Then
                thkM = smData.thickness
                hasThkM = True
            End If
        End If
        Set feat = feat.GetNextFeature
    Loop

    isSm = hasFlat
    If hasThkM Then
        thkMm = thkM * API_TO_MM
        hasThk = (thkMm > 0)
        thickness = FmtNum(thkMm, iDecimalPlaces) & " mm"
    End If

    matName = ""
    database = ""
    matName = swModel.GetMaterialPropertyName2("", database)
    If Len(Trim$(matName)) > 0 Then material = matName

    If material <> UNDEF Then
        Set ext = swModel.Extension
        If Not ext Is Nothing Then
            Set massProp = ext.CreateMassProperty
            If Not massProp Is Nothing Then
                mass = massProp.mass
                If Err.Number = 0 And mass >= 0 Then
                    hasWeight = True
                    weightKg = mass
                    weight = FmtNum(mass, iDecimalPlaces) & " kg"
                End If
                Err.Clear
            End If
        End If
    End If
    On Error GoTo 0
End Sub

'==============================================================================
' FLATTEN / GEOMETRY
'==============================================================================
Private Function CanFlatten(ByRef msg As String) As Boolean
    Dim flatFeat As Object
    Dim wasSuppressed As Boolean
    Dim flatBody As Object
    Dim ok As Boolean

    CanFlatten = False
    msg = ""

    If swModel.GetType <> swDocPART Then
        msg = "Active document is not a part."
        Exit Function
    End If

    Set flatFeat = GetFlatPatternFeature()
    If flatFeat Is Nothing Then
        msg = "Part is not sheet metal or has no flat pattern."
        Exit Function
    End If

    If Not TryGetSuppressed(flatFeat, wasSuppressed) Then
        msg = "Cannot check Flat Pattern state."
        Exit Function
    End If

    On Error GoTo Fail
    If wasSuppressed Then
        ok = flatFeat.SetSuppression2(swUnSuppressFeature, swThisConfiguration, Empty)
        If Not ok Then
            msg = "Cannot flatten this part: Flat-Pattern/Flatten operation is unavailable."
            Exit Function
        End If
        swModel.ForceRebuild3 False
        If Not TryGetSuppressed(flatFeat, ok) Or ok Then
            FoldBack flatFeat, wasSuppressed
            msg = "Cannot flatten this part: Flat-Pattern stayed folded after flatten command."
            Exit Function
        End If
    End If

    Set flatBody = GetFlatPatternBody()
    FoldBack flatFeat, wasSuppressed

    If flatBody Is Nothing Then
        msg = "Cannot flatten this part: Flat pattern body is unavailable."
        Exit Function
    End If

    CanFlatten = True
    msg = "Flat pattern is available."
    Exit Function

Fail:
    FoldBack flatFeat, wasSuppressed
    msg = "Cannot flatten this part: an error occurred while preparing Flat Pattern."
    CanFlatten = False
End Function

Private Function CalcOnFlatPattern(ByRef areaMm2 As Double, ByRef hasArea As Boolean, _
                                   ByRef blankL As Double, ByRef blankW As Double, ByRef blankA As Double, ByRef hasBlank As Boolean, _
                                   ByRef laserMm As Double, ByRef hasLaser As Boolean, _
                                   ByRef bendCnt As Long, ByRef hasBendCnt As Boolean, _
                                   ByRef bendLenMm As Double, ByRef hasBendLen As Boolean, _
                                   ByRef hasOverlap As Boolean, ByRef overlapMsg As String) As Boolean
    Dim flatFeat As Object
    Dim wasSuppressed As Boolean
    Dim flatBody As Object
    Dim ok As Boolean
    Dim checkRes As Long

    CalcOnFlatPattern = False
    hasArea = False: hasBlank = False: hasLaser = False
    hasBendCnt = False: hasBendLen = False
    hasOverlap = False
    overlapMsg = ""

    Set flatFeat = GetFlatPatternFeature()
    If flatFeat Is Nothing Then Exit Function
    If Not TryGetSuppressed(flatFeat, wasSuppressed) Then Exit Function

    On Error GoTo Fail
    If wasSuppressed Then
        ok = flatFeat.SetSuppression2(swUnSuppressFeature, swThisConfiguration, Empty)
        If Not ok Then Exit Function
        swModel.ForceRebuild3 False
        If Not TryGetSuppressed(flatFeat, ok) Or ok Then
            FoldBack flatFeat, wasSuppressed
            Exit Function
        End If
    End If

    Set flatBody = GetFlatPatternBody()
    If flatBody Is Nothing Then
        FoldBack flatFeat, wasSuppressed
        Exit Function
    End If

    ' Tools > Evaluate > Check equivalent
    On Error Resume Next
    Err.Clear
    checkRes = flatBody.Check2
    If Err.Number <> 0 Then
        Err.Clear
        overlapMsg = "Check could not be run."
        hasOverlap = False
    ElseIf checkRes <> 0 Then
        hasOverlap = True
        overlapMsg = "Geometry check found issues (Tools > Evaluate > Check). Fix errors in the flat pattern."
        FoldBack flatFeat, wasSuppressed
        CalcOnFlatPattern = True
        Exit Function
    Else
        hasOverlap = False
        overlapMsg = "No overlaps detected in the flat sheet (Check passed)."
    End If
    On Error GoTo Fail

    hasArea = TryPlanarAreaMm2(flatBody, areaMm2)
    hasBlank = TryBlankArea(flatBody, blankL, blankW, blankA)
    hasLaser = TryLaserPathMm(flatBody, laserMm)
    hasBendCnt = True
    bendCnt = GetBendLinesCount()
    hasBendLen = True
    bendLenMm = GetBendLinesLengthMm(IIf(hasLaser, laserMm, -1#), hasLaser)

    FoldBack flatFeat, wasSuppressed
    CalcOnFlatPattern = True
    Exit Function

Fail:
    FoldBack flatFeat, wasSuppressed
    CalcOnFlatPattern = False
End Function

Private Sub FoldBack(ByVal flatFeat As Object, ByVal wasSuppressed As Boolean)
    On Error Resume Next
    If wasSuppressed And Not flatFeat Is Nothing Then
        flatFeat.SetSuppression2 swSuppressFeature, swThisConfiguration, Empty
    End If
    Err.Clear
End Sub

Private Function TryGetSuppressed(ByVal feat As Object, ByRef isSuppressed As Boolean) As Boolean
    Dim v As Variant
    On Error Resume Next
    Err.Clear
    v = feat.IsSuppressed2(swThisConfiguration, Empty)
    If Err.Number <> 0 Then
        Err.Clear
        TryGetSuppressed = False
        Exit Function
    End If
    If IsArray(v) Then
        isSuppressed = CBool(v(0))
        TryGetSuppressed = True
    Else
        isSuppressed = CBool(v)
        TryGetSuppressed = True
    End If
End Function

Private Function GetFlatPatternFeature() As Object
    Dim feat As Object
    Set GetFlatPatternFeature = Nothing
    On Error Resume Next
    Set feat = swModel.FirstFeature
    Do While Not feat Is Nothing
        If StrComp(feat.GetTypeName2, "FlatPattern", vbTextCompare) = 0 Then
            Set GetFlatPatternFeature = feat
            Exit Function
        End If
        Set feat = feat.GetNextFeature
    Loop
End Function

Private Function GetFlatPatternBody() As Object
    Dim feat As Object
    Dim fpData As Object
    Dim fixedFace As Object
    Set GetFlatPatternBody = Nothing
    On Error Resume Next
    Set feat = GetFlatPatternFeature()
    If feat Is Nothing Then Exit Function
    Set fpData = feat.GetDefinition
    If fpData Is Nothing Then Exit Function
    Set fixedFace = fpData.FixedFace2
    If fixedFace Is Nothing Then Exit Function
    Set GetFlatPatternBody = fixedFace.GetBody
End Function

Private Function TryPlanarAreaMm2(ByVal body As Object, ByRef areaMm2 As Double) As Boolean
    Dim faces As Variant
    Dim i As Long
    Dim face As Object
    Dim a As Double, maxA As Double

    TryPlanarAreaMm2 = False
    On Error Resume Next
    faces = body.GetFaces
    If Err.Number <> 0 Or IsEmpty(faces) Then
        Err.Clear
        Exit Function
    End If
    maxA = 0#
    For i = 0 To UBound(faces)
        Set face = faces(i)
        If Not face Is Nothing Then
            a = face.GetArea
            If a > maxA Then maxA = a
        End If
    Next i
    If maxA <= 0 Then Exit Function
    areaMm2 = maxA * 1000000#
    TryPlanarAreaMm2 = True
End Function

Private Function TryBlankArea(ByVal body As Object, ByRef lengthMm As Double, _
                              ByRef widthMm As Double, ByRef areaMm2 As Double) As Boolean
    Dim boxObj As Variant
    Dim box() As Double
    Dim dx As Double, dy As Double, dz As Double
    Dim d(0 To 2) As Double
    Dim allow As Double

    TryBlankArea = False
    allow = dMaterialAllowanceMm
    On Error Resume Next
    boxObj = body.GetBodyBox
    If Err.Number <> 0 Or IsEmpty(boxObj) Then
        Err.Clear
        Exit Function
    End If
    If Not IsArray(boxObj) Then Exit Function
    If UBound(boxObj) < 5 Then Exit Function

    dx = (CDbl(boxObj(3)) - CDbl(boxObj(0))) * API_TO_MM
    dy = (CDbl(boxObj(4)) - CDbl(boxObj(1))) * API_TO_MM
    dz = (CDbl(boxObj(5)) - CDbl(boxObj(2))) * API_TO_MM

    If dz > dx Or dz > dy Then
        d(0) = dx: d(1) = dy: d(2) = dz
        Sort3 d
        lengthMm = d(2) + 2# * allow
        widthMm = d(1) + 2# * allow
    Else
        If dx >= dy Then
            lengthMm = dx + 2# * allow
            widthMm = dy + 2# * allow
        Else
            lengthMm = dy + 2# * allow
            widthMm = dx + 2# * allow
        End If
    End If
    areaMm2 = lengthMm * widthMm
    TryBlankArea = True
End Function

Private Sub Sort3(ByRef d() As Double)
    Dim t As Double
    If d(0) > d(1) Then t = d(0): d(0) = d(1): d(1) = t
    If d(1) > d(2) Then t = d(1): d(1) = d(2): d(2) = t
    If d(0) > d(1) Then t = d(0): d(0) = d(1): d(1) = t
End Sub

Private Function TryLaserPathMm(ByVal flatBody As Object, ByRef laserMm As Double) As Boolean
    Dim mainFace As Object
    Dim selMgr As Object
    Dim selData As Object
    Dim ext As Object
    Dim measure As Object
    Dim ok As Boolean
    Dim periM As Double
    Dim ent As Object

    TryLaserPathMm = False
    Set mainFace = GetMaxAreaFace(flatBody)
    If mainFace Is Nothing Then Exit Function

    On Error Resume Next
    swModel.ClearSelection2 True
    Set selMgr = swModel.SelectionManager
    If selMgr Is Nothing Then Exit Function
    Set selData = selMgr.CreateSelectData
    Set ent = mainFace
    ok = ent.Select4(False, selData)
    If Not ok Then
        swModel.ClearSelection2 True
        Exit Function
    End If

    Set ext = swModel.Extension
    If ext Is Nothing Then
        swModel.ClearSelection2 True
        Exit Function
    End If
    Set measure = ext.CreateMeasure
    If measure Is Nothing Then
        swModel.ClearSelection2 True
        Exit Function
    End If

    Err.Clear
    ok = measure.Calculate(Array(mainFace))
    swModel.ClearSelection2 True
    If Err.Number <> 0 Or Not ok Then
        Err.Clear
        Exit Function
    End If
    periM = measure.Perimeter
    If periM <= 0 Then Exit Function
    laserMm = periM * API_TO_MM
    TryLaserPathMm = True
End Function

Private Function GetMaxAreaFace(ByVal body As Object) As Object
    Dim faces As Variant
    Dim i As Long
    Dim face As Object
    Dim a As Double, maxA As Double
    Dim best As Object

    Set GetMaxAreaFace = Nothing
    On Error Resume Next
    faces = body.GetFaces
    If IsEmpty(faces) Then Exit Function
    maxA = 0#
    For i = 0 To UBound(faces)
        Set face = faces(i)
        If Not face Is Nothing Then
            a = face.GetArea
            If a > maxA Then
                maxA = a
                Set best = face
            End If
        End If
    Next i
    Set GetMaxAreaFace = best
End Function

'==============================================================================
' BEND LINES (same rules as SheetMetalCalcService)
'==============================================================================
Private Function GetBendLinesCount() As Long
    Dim flatFeat As Object
    Dim subFeat As Object
    Dim sketch As Object
    Dim total As Long
    Dim feat As Object
    Dim adjusted As Long

    total = 0
    On Error Resume Next
    Set flatFeat = GetFlatPatternFeature()
    If Not flatFeat Is Nothing Then
        Set subFeat = flatFeat.GetFirstSubFeature
        Do While Not subFeat Is Nothing
            Set sketch = Nothing
            Set sketch = subFeat.GetSpecificFeature2
            ' Late binding: any object with GetSketchSegments is treated as a sketch
            If Not sketch Is Nothing Then
                total = total + CountBendGroupsInSketch(sketch)
            End If
            Set subFeat = subFeat.GetNextSubFeature
        Loop
    End If

    If total = 0 Then
        Set feat = swModel.FirstFeature
        Do While Not feat Is Nothing
            If StrComp(feat.GetTypeName2, "SM3dBend", vbTextCompare) = 0 Then
                total = total + CountSketchedBend(feat)
            End If
            Set feat = feat.GetNextFeature
        Loop
    End If

    ' Panel subtracts 4 helper construction segments typical for flat-pattern sketches
    adjusted = total - 4
    If adjusted < 0 Then adjusted = 0
    GetBendLinesCount = adjusted
End Function

Private Function GetBendLinesLengthMm(ByVal laserMm As Double, ByVal hasLaser As Boolean) As Double
    Dim flatFeat As Object
    Dim subFeat As Object
    Dim sketch As Object
    Dim totalM As Double
    Dim feat As Object
    Dim totalMm As Double

    totalM = 0#
    On Error Resume Next
    Set flatFeat = GetFlatPatternFeature()
    If Not flatFeat Is Nothing Then
        Set subFeat = flatFeat.GetFirstSubFeature
        Do While Not subFeat Is Nothing
            Set sketch = subFeat.GetSpecificFeature2
            If Not sketch Is Nothing Then
                totalM = totalM + SketchConstructionLengthM(sketch)
            End If
            Set subFeat = subFeat.GetNextSubFeature
        Loop
    End If

    If totalM <= 0# Then
        Set feat = swModel.FirstFeature
        Do While Not feat Is Nothing
            If StrComp(feat.GetTypeName2, "SM3dBend", vbTextCompare) = 0 Then
                totalM = totalM + SketchedBendLengthM(feat)
            End If
            Set feat = feat.GetNextFeature
        Loop
    End If

    If totalM <= 0# Then
        GetBendLinesLengthMm = 0#
        Exit Function
    End If

    totalMm = totalM * API_TO_MM
    ' Subtract perimeter if construction sketches also contain cut contour
    If hasLaser And laserMm > 0 And totalMm > laserMm Then
        totalMm = totalMm - laserMm
    End If
    If totalMm < 0 Then totalMm = 0#
    GetBendLinesLengthMm = totalMm
End Function

Private Function CountSketchedBend(ByVal bendFeat As Object) As Long
    Dim subFeat As Object
    Dim sketch As Object
    CountSketchedBend = 0
    On Error Resume Next
    Set subFeat = bendFeat.GetFirstSubFeature
    Do While Not subFeat Is Nothing
        If StrComp(subFeat.GetTypeName2, "ProfileFeature", vbTextCompare) = 0 Then
            Set sketch = subFeat.GetSpecificFeature2
            If Not sketch Is Nothing Then
                CountSketchedBend = CountBendGroupsInSketch(sketch)
                Exit Function
            End If
        End If
        Set subFeat = subFeat.GetNextSubFeature
    Loop
End Function

Private Function SketchedBendLengthM(ByVal bendFeat As Object) As Double
    Dim subFeat As Object
    Dim sketch As Object
    SketchedBendLengthM = 0#
    On Error Resume Next
    Set subFeat = bendFeat.GetFirstSubFeature
    Do While Not subFeat Is Nothing
        If StrComp(subFeat.GetTypeName2, "ProfileFeature", vbTextCompare) = 0 Then
            Set sketch = subFeat.GetSpecificFeature2
            If Not sketch Is Nothing Then
                SketchedBendLengthM = SketchConstructionLengthM(sketch)
                Exit Function
            End If
        End If
        Set subFeat = subFeat.GetNextSubFeature
    Loop
End Function

Private Function SketchConstructionLengthM(ByVal sketch As Object) As Double
    Dim segs As Variant
    Dim i As Long
    Dim seg As Object
    Dim line As Object
    Dim sp As Object, ep As Object
    Dim dx As Double, dy As Double, dz As Double, ln As Double
    Dim sum As Double

    sum = 0#
    On Error Resume Next
    segs = sketch.GetSketchSegments
    If IsEmpty(segs) Then
        SketchConstructionLengthM = 0#
        Exit Function
    End If
    For i = 0 To UBound(segs)
        Set seg = segs(i)
        If Not seg Is Nothing Then
            If seg.GetType = swSketchLINE And seg.ConstructionGeometry Then
                Set line = seg
                Set sp = line.GetStartPoint2
                Set ep = line.GetEndPoint2
                If Not sp Is Nothing And Not ep Is Nothing Then
                    dx = ep.X - sp.X
                    dy = ep.Y - sp.Y
                    dz = ep.Z - sp.Z
                    ln = Sqr(dx * dx + dy * dy + dz * dz)
                    If ln > 0 Then sum = sum + ln
                End If
            End If
        End If
    Next i
    SketchConstructionLengthM = sum
End Function

' Group colinear connected construction lines into bend counts (BFS)
Private Function CountBendGroupsInSketch(ByVal sketch As Object) As Long
    Dim segs As Variant
    Dim n As Long, i As Long, j As Long, k As Long
    Dim seg As Object, line As Object, sp As Object, ep As Object
    Dim isLine() As Boolean
    Dim p1x() As Double, p1y() As Double, p1z() As Double
    Dim p2x() As Double, p2y() As Double, p2z() As Double
    Dim dx() As Double, dy() As Double, dz() As Double
    Dim visited() As Boolean
    Dim queue() As Long
    Dim qHead As Long, qTail As Long
    Dim groups As Long
    Dim vx As Double, vy As Double, vz As Double, ln As Double
    Dim dot As Double
    Dim tol As Double, cosTol As Double

    CountBendGroupsInSketch = 0
    tol = 0.000001
    cosTol = 0.9999

    On Error Resume Next
    segs = sketch.GetSketchSegments
    If IsEmpty(segs) Then Exit Function
    n = UBound(segs) - LBound(segs) + 1
    If n <= 0 Then Exit Function

    ReDim isLine(0 To n - 1)
    ReDim p1x(0 To n - 1): ReDim p1y(0 To n - 1): ReDim p1z(0 To n - 1)
    ReDim p2x(0 To n - 1): ReDim p2y(0 To n - 1): ReDim p2z(0 To n - 1)
    ReDim dx(0 To n - 1): ReDim dy(0 To n - 1): ReDim dz(0 To n - 1)
    ReDim visited(0 To n - 1)
    ReDim queue(0 To n - 1)

    For i = 0 To n - 1
        Set seg = segs(LBound(segs) + i)
        isLine(i) = False
        If Not seg Is Nothing Then
            If seg.GetType = swSketchLINE And seg.ConstructionGeometry Then
                Set line = seg
                Set sp = line.GetStartPoint2
                Set ep = line.GetEndPoint2
                If Not sp Is Nothing And Not ep Is Nothing Then
                    p1x(i) = sp.X: p1y(i) = sp.Y: p1z(i) = sp.Z
                    p2x(i) = ep.X: p2y(i) = ep.Y: p2z(i) = ep.Z
                    vx = p2x(i) - p1x(i)
                    vy = p2y(i) - p1y(i)
                    vz = p2z(i) - p1z(i)
                    ln = Sqr(vx * vx + vy * vy + vz * vz)
                    If ln > tol Then
                        dx(i) = vx / ln
                        dy(i) = vy / ln
                        dz(i) = vz / ln
                        isLine(i) = True
                    End If
                End If
            End If
        End If
    Next i

    groups = 0
    For i = 0 To n - 1
        If isLine(i) And Not visited(i) Then
            groups = groups + 1
            qHead = 0: qTail = 0
            visited(i) = True
            queue(qTail) = i: qTail = qTail + 1
            Do While qHead < qTail
                j = queue(qHead): qHead = qHead + 1
                For k = 0 To n - 1
                    If isLine(k) And Not visited(k) Then
                        If ShareEndpoint(p1x(j), p1y(j), p1z(j), p2x(j), p2y(j), p2z(j), _
                                         p1x(k), p1y(k), p1z(k), p2x(k), p2y(k), p2z(k), tol) Then
                            dot = dx(j) * dx(k) + dy(j) * dy(k) + dz(j) * dz(k)
                            If Abs(dot) >= cosTol Then
                                visited(k) = True
                                queue(qTail) = k: qTail = qTail + 1
                            End If
                        End If
                    End If
                Next k
            Loop
        End If
    Next i

    CountBendGroupsInSketch = groups
End Function

Private Function ShareEndpoint(ByVal a1x As Double, ByVal a1y As Double, ByVal a1z As Double, _
                               ByVal a2x As Double, ByVal a2y As Double, ByVal a2z As Double, _
                               ByVal b1x As Double, ByVal b1y As Double, ByVal b1z As Double, _
                               ByVal b2x As Double, ByVal b2y As Double, ByVal b2z As Double, _
                               ByVal tol As Double) As Boolean
    Dim t2 As Double
    t2 = tol * tol
    ShareEndpoint = (Dist2(a1x, a1y, a1z, b1x, b1y, b1z) <= t2) Or _
                    (Dist2(a1x, a1y, a1z, b2x, b2y, b2z) <= t2) Or _
                    (Dist2(a2x, a2y, a2z, b1x, b1y, b1z) <= t2) Or _
                    (Dist2(a2x, a2y, a2z, b2x, b2y, b2z) <= t2)
End Function

Private Function Dist2(ByVal x1 As Double, ByVal y1 As Double, ByVal z1 As Double, _
                       ByVal x2 As Double, ByVal y2 As Double, ByVal z2 As Double) As Double
    Dim dx As Double, dy As Double, dz As Double
    dx = x1 - x2: dy = y1 - y2: dz = z1 - z2
    Dist2 = dx * dx + dy * dy + dz * dz
End Function

'==============================================================================
' PRICING (same formulas as ExportTaskPaneControl)
'==============================================================================
Private Function CalcMaterialCost(ByVal hasWeight As Boolean, ByVal weightKg As Double, _
                                  ByVal hasArea As Boolean, ByVal areaMm2 As Double, _
                                  ByVal hasBlank As Boolean, ByVal blankAreaMm2 As Double, _
                                  ByVal hasThk As Boolean, ByVal thkMm As Double) As Variant
    Dim blankMass As Double
    CalcMaterialCost = Empty

    If bMaterialNet Then
        If bMaterialPricePerKg Then
            If hasWeight And weightKg >= 0 Then CalcMaterialCost = weightKg * dMaterialPrice
        Else
            If hasArea And areaMm2 > 0 Then CalcMaterialCost = (areaMm2 / 1000000#) * dMaterialPrice
        End If
    Else
        If bMaterialPricePerKg Then
            If hasBlank And blankAreaMm2 > 0 And hasArea And areaMm2 > 0 And hasWeight And weightKg >= 0 Then
                blankMass = weightKg * (blankAreaMm2 / areaMm2)
                CalcMaterialCost = blankMass * dMaterialPrice
            End If
        Else
            If hasBlank And blankAreaMm2 > 0 Then CalcMaterialCost = (blankAreaMm2 / 1000000#) * dMaterialPrice
        End If
    End If
End Function

Private Function CalcCutCost(ByVal hasLaser As Boolean, ByVal laserMm As Double, _
                             ByVal hasThk As Boolean, ByVal thkMm As Double) As Variant
    CalcCutCost = Empty
    If Not hasLaser Or laserMm <= 0 Then Exit Function
    If Not hasThk Or thkMm <= 0 Then Exit Function
    If dPricePerMmCutPer1MmThickness <= 0 Then Exit Function
    CalcCutCost = laserMm * thkMm * dPricePerMmCutPer1MmThickness
End Function

Private Function CalcBendCost(ByVal hasCnt As Boolean, ByVal cnt As Long) As Variant
    CalcBendCost = Empty
    If Not hasCnt Or cnt <= 0 Then Exit Function
    If dPricePerBend <= 0 Then Exit Function
    CalcBendCost = CDbl(cnt) * dPricePerBend
End Function

Private Function CalcEngraverCost(ByVal hasLen As Boolean, ByVal lenMm As Double) As Variant
    CalcEngraverCost = Empty
    If Not hasLen Or lenMm <= 0 Then Exit Function
    If dPricePerMmBendLineEngraver <= 0 Then Exit Function
    CalcEngraverCost = lenMm * dPricePerMmBendLineEngraver
End Function

Private Function FormatTotalPrice(ByVal matCost As Variant, ByVal cutCost As Variant, _
                                  ByVal bendCost As Variant, ByVal engCost As Variant) As String
    Dim total As Double
    Dim workOnly As Double

    workOnly = 0#
    If Not IsEmpty(cutCost) Then workOnly = workOnly + CDbl(cutCost)
    If Not IsEmpty(bendCost) Then workOnly = workOnly + CDbl(bendCost)
    If Not IsEmpty(engCost) Then workOnly = workOnly + CDbl(engCost)

    If IsEmpty(matCost) Then
        FormatTotalPrice = FmtNum(workOnly, 2) & " (work price, material excluded)"
        Exit Function
    End If

    total = CDbl(matCost) + workOnly
    FormatTotalPrice = FmtNum(total, 2)
End Function

'==============================================================================
' TXT EXPORT
'==============================================================================
Private Function CanWriteNextToModel() As Boolean
    Dim p As String
    On Error Resume Next
    p = swModel.GetPathName
    CanWriteNextToModel = (Len(p) > 0)
End Function

Private Function BuildTxtPath() As String
    Dim docPath As String
    Dim dir As String, baseName As String
    Dim material As String, thickness As String
    Dim stamp As String
    Dim isSm As Boolean, partName As String, thk As String, mat As String, w As String
    Dim hasW As Boolean, wKg As Double, hasT As Boolean, tMm As Double

    BuildTxtPath = ""
    On Error Resume Next
    docPath = swModel.GetPathName
    If Len(docPath) = 0 Then Exit Function

    GetPartInfo isSm, partName, thk, mat, w, hasW, wKg, hasT, tMm
    material = SafeFilePart(mat)
    thickness = SafeFilePart(thk)

    dir = Left$(docPath, InStrRev(docPath, "\") - 1)
    baseName = Mid$(docPath, InStrRev(docPath, "\") + 1)
    If InStrRev(baseName, ".") > 0 Then baseName = Left$(baseName, InStrRev(baseName, ".") - 1)

    stamp = Format$(Now, "yyyy-mm-dd_hh-nn-ss")
    BuildTxtPath = dir & "\" & baseName & "_" & material & "_" & thickness & "_" & stamp & ".txt"
End Function

Private Function SafeFilePart(ByVal value As String) As String
    Dim s As String
    Dim i As Long
    Dim ch As String
    Dim bad As String

    If Len(Trim$(value)) = 0 Or value = UNDEF Then
        SafeFilePart = "unknown"
        Exit Function
    End If
    s = Trim$(value)
    bad = "\:*?""<>|/"
    For i = 1 To Len(bad)
        ch = Mid$(bad, i, 1)
        s = Replace(s, ch, "_")
    Next i
    s = Replace(s, " ", "_")
    Do While InStr(s, "__") > 0
        s = Replace(s, "__", "_")
    Loop
    If Len(s) = 0 Then s = "unknown"
    SafeFilePart = s
End Function

Private Function WriteTextFile(ByVal path As String, ByVal content As String) As Boolean
    Dim fso As Object
    Dim ts As Object
    On Error GoTo Fail
    Set fso = CreateObject("Scripting.FileSystemObject")
    Set ts = fso.CreateTextFile(path, True, True) ' Unicode
    ts.Write content
    ts.Close
    WriteTextFile = True
    Exit Function
Fail:
    WriteTextFile = False
End Function
'==============================================================================
' SheetMetalCalc.bas — SolidWorks VBA macro
' Mirrors "Preliminary Cost Estimate" panel (Solidworks panel add-in).
'
' WHAT THIS MACRO DOES
'   For the active sheet-metal Part:
'     - reads part / thickness / material / weight
'     - temporarily flattens FlatPattern
'     - measures area, blank (+ allowance), laser path, bend count / length
'     - checks flat-body geometry (Body.Check2)
'     - computes material / cut / bend / engraver / total price
'   Shows the same text report as the panel (MsgBox) and optionally saves .txt
'   next to the model: {name}_{material}_{thickness}_{yyyy-MM-dd_HH-mm-ss}.txt
'
' HOW TO USE
'   1) Edit SETTINGS below (prices, allowance, export flags).
'   2) Open a saved sheet-metal Part in SolidWorks.
'   3) Tools > Macro > Run > select this .bas (entry point: main).
'
' NOTES
'   - Late binding (Object) — no SolidWorks type library required.
'   - FlatPattern is restored to the previous suppressed/unsuppressed state.
'   - Settings mirror CostSettingsData defaults from the panel add-in.
'==============================================================================

Option Explicit

'----- SETTINGS (edit these — same meaning as panel Cost Settings) ------------
' Values below match a typical panel preset; dump of these is appended to report
' under "Settings" so a .txt export can restore the calculation inputs later.

Private Const iDecimalPlaces As Integer = 2
' Places after decimal for geometry values (price always 2). Panel: Decimal places.

Private Const dMaterialAllowanceMm As Double = 1#
' Blank allowance per side, mm. Panel: Material allowance (mm).

Private Const dPricePerBend As Double = 2#
' Price for one bend. Panel: Price per 1 bend.

Private Const dPricePerMmBendLineEngraver As Double = 0.1
' Price per 1 mm of bend-line engraver path. Panel: Price per 1 mm bend line.

Private Const dPricePerMmCutPer1MmThickness As Double = 13.55
' Cut price per 1 mm path at 1 mm thickness. Panel: Price per 1 mm cut.

Private Const dMaterialPrice As Double = 12#
' Material unit price. Panel: Material price.

Private Const bMaterialPricePerKg As Boolean = True
' True = Per 1 kg; False = Per 1 m?. Panel: Material price unit.

Private Const bMaterialNet As Boolean = False
' True = Area (Net); False = Blank (Gross). Panel: Material quantity.

Private Const bExportTxt As Boolean = True
' True = write report .txt next to the part file.

Private Const iNotify As Integer = 1
' 0 = MsgBox only on failure; 1 = always show report MsgBox.

'----- SW API constants -------------------------------------------------------
Private Const swDocPART As Long = 1
Private Const swUnSuppressFeature As Long = 2
Private Const swSuppressFeature As Long = 0
Private Const swThisConfiguration As Long = 1
Private Const swSketchLINE As Long = 0
Private Const API_TO_MM As Double = 1000#

Private Const KEY_COL As Integer = 12
Private Const SETTINGS_KEY_COL As Integer = 24
Private Const MODULE_SEP As String = "---"
Private Const UNDEF As String = "undefined"

'----- Module state -----------------------------------------------------------
Private swApp As Object
Private swModel As Object

'==============================================================================
' ENTRY
'==============================================================================
Public Sub main()
    Dim report As String
    Dim path As String
    Dim ok As Boolean

    On Error GoTo Fatal
    Set swApp = Application.SldWorks
    If swApp Is Nothing Then Set swApp = GetObject(, "SldWorks.Application")
    If swApp Is Nothing Then
        MsgBox "SolidWorks is not running.", vbExclamation, "SheetMetalCalc"
        Exit Sub
    End If

    Set swModel = swApp.ActiveDoc
    If swModel Is Nothing Then
        MsgBox "No document open.", vbExclamation, "SheetMetalCalc"
        Exit Sub
    End If

    report = BuildReport()
    ok = (Len(report) > 0) And (InStr(1, report, "No document", vbTextCompare) = 0)

    If bExportTxt And ok And CanWriteNextToModel() Then
        path = BuildTxtPath()
        If Len(path) > 0 Then
            If WriteTextFile(path, report) Then
                report = report & vbCrLf & MODULE_SEP & vbCrLf & _
                         PadKey("export") & " - " & path
            Else
                report = report & vbCrLf & MODULE_SEP & vbCrLf & _
                         "Export failed: could not write TXT."
                ok = False
            End If
        End If
    End If

    If iNotify = 1 Or Not ok Then
        MsgBox report, IIf(ok, vbInformation, vbExclamation), "SheetMetalCalc"
    End If
    Exit Sub

Fatal:
    MsgBox "SheetMetalCalc error: " & Err.Description, vbCritical, "SheetMetalCalc"
End Sub

'==============================================================================
' REPORT (panel-compatible layout)
'==============================================================================
Private Function BuildReport() As String
    Dim modules As String
    Dim infoPart As String, infoThk As String, infoMat As String, infoW As String
    Dim isSm As Boolean
    Dim canMsg As String
    Dim areaMm2 As Double, blankL As Double, blankW As Double, blankA As Double
    Dim laserMm As Double, bendLenMm As Double
    Dim bendCnt As Long
    Dim hasArea As Boolean, hasBlank As Boolean, hasLaser As Boolean
    Dim hasBendCnt As Boolean, hasBendLen As Boolean
    Dim hasOverlap As Boolean, overlapMsg As String
    Dim weightKg As Double, thkMm As Double
    Dim hasWeight As Boolean, hasThk As Boolean
    Dim matCost As Variant, cutCost As Variant, bendCost As Variant, engCost As Variant
    Dim totalTxt As String
    Dim sheetBlock As String
    Dim engBlock As String

    GetPartInfo isSm, infoPart, infoThk, infoMat, infoW, hasWeight, weightKg, hasThk, thkMm

    If Not isSm Then
        BuildReport = "Document is not a sheet part." & vbCrLf & _
                      "(Open a part with Flat Pattern / sheet metal.)" & _
                      vbCrLf & MODULE_SEP & vbCrLf & BuildSettingsBlock()
        Exit Function
    End If

    If Not CanFlatten(canMsg) Then
        BuildReport = PadKey("part") & " - " & infoPart & " loaded" & vbCrLf & _
                      PadKey("thickness") & " - " & infoThk & vbCrLf & _
                      PadKey("material") & " - " & infoMat & vbCrLf & _
                      IIf(hasWeight, PadKey("weight") & " - " & infoW & vbCrLf, "") & _
                      PadKey("price") & " - (not calculated)" & vbCrLf & _
                      MODULE_SEP & vbCrLf & canMsg & _
                      vbCrLf & MODULE_SEP & vbCrLf & BuildSettingsBlock()
        Exit Function
    End If

    If Not CalcOnFlatPattern(areaMm2, hasArea, blankL, blankW, blankA, hasBlank, _
                             laserMm, hasLaser, bendCnt, hasBendCnt, bendLenMm, hasBendLen, _
                             hasOverlap, overlapMsg) Then
        BuildReport = PadKey("part") & " - " & infoPart & " loaded" & vbCrLf & _
                      PadKey("thickness") & " - " & infoThk & vbCrLf & _
                      PadKey("material") & " - " & infoMat & vbCrLf & _
                      IIf(hasWeight, PadKey("weight") & " - " & infoW & vbCrLf, "") & _
                      PadKey("price") & " - (not calculated)" & vbCrLf & _
                      MODULE_SEP & vbCrLf & _
                      "Cannot flatten this part: Flat-Pattern/Flatten operation is unavailable." & _
                      vbCrLf & MODULE_SEP & vbCrLf & BuildSettingsBlock()
        Exit Function
    End If

    If hasOverlap Then
        BuildReport = PadKey("part") & " - " & infoPart & " loaded" & vbCrLf & _
                      PadKey("thickness") & " - " & infoThk & vbCrLf & _
                      PadKey("material") & " - " & infoMat & vbCrLf & _
                      IIf(hasWeight, PadKey("weight") & " - " & infoW & vbCrLf, "") & _
                      PadKey("price") & " - (not calculated)" & vbCrLf & _
                      MODULE_SEP & vbCrLf & _
                      IIf(Len(overlapMsg) > 0, overlapMsg, "Geometry check found issues in flat pattern.") & _
                      vbCrLf & MODULE_SEP & vbCrLf & BuildSettingsBlock()
        Exit Function
    End If

    If hasBendCnt And bendCnt <= 0 Then
        bendLenMm = 0#
        hasBendLen = True
    End If

    matCost = CalcMaterialCost(hasWeight, weightKg, hasArea, areaMm2, hasBlank, blankA, hasThk, thkMm)
    cutCost = CalcCutCost(hasLaser, laserMm, hasThk, thkMm)
    bendCost = CalcBendCost(hasBendCnt, bendCnt)
    engCost = CalcEngraverCost(hasBendLen, bendLenMm)
    totalTxt = FormatTotalPrice(matCost, cutCost, bendCost, engCost)

    ' Module 1 — part summary
    modules = PadKey("part") & " - " & infoPart & " loaded" & vbCrLf & _
              PadKey("thickness") & " - " & infoThk & vbCrLf & _
              PadKey("material") & " - " & infoMat & vbCrLf
    If hasWeight Then modules = modules & PadKey("weight") & " - " & infoW & vbCrLf
    modules = modules & PadKey("price") & " - " & totalTxt

    ' Module 2 — sheet / laser / bends
    sheetBlock = "Sheet OK" & vbCrLf
    If hasArea Then
        sheetBlock = sheetBlock & PadKey("area") & " - " & FmtNum(areaMm2, iDecimalPlaces) & " mm?" & vbCrLf
    End If
    If hasBlank Then
        sheetBlock = sheetBlock & PadKey("blank") & " - " & _
                     FmtNum(blankL, iDecimalPlaces) & " x " & FmtNum(blankW, iDecimalPlaces) & " mm (" & _
                     FmtNum(blankA, iDecimalPlaces) & " mm?), allowance +" & _
                     FmtNumG(dMaterialAllowanceMm) & " mm per side (from settings)" & vbCrLf
    End If
    sheetBlock = sheetBlock & PadKey("material price") & " - " & FormatMoney(matCost) & vbCrLf
    sheetBlock = sheetBlock & MODULE_SEP & vbCrLf & "Laser cut" & vbCrLf
    If hasLaser Then
        sheetBlock = sheetBlock & PadKey("length") & " - " & FmtNum(laserMm, iDecimalPlaces) & " mm" & vbCrLf
        sheetBlock = sheetBlock & PadKey("cutting price") & " - " & FormatMoney(cutCost) & vbCrLf
    Else
        sheetBlock = sheetBlock & PadKey("length") & " - —" & vbCrLf
        sheetBlock = sheetBlock & PadKey("cutting price") & " - (not calculated)" & vbCrLf
    End If
    sheetBlock = sheetBlock & MODULE_SEP & vbCrLf & "Bends" & vbCrLf
    sheetBlock = sheetBlock & PadKey("count") & " - " & CStr(IIf(hasBendCnt, bendCnt, 0)) & vbCrLf
    sheetBlock = sheetBlock & PadKey("bending price") & " - " & FormatMoney(bendCost)

    modules = modules & vbCrLf & MODULE_SEP & vbCrLf & sheetBlock

    ' Module 3 — engraver
    engBlock = "Engraver" & vbCrLf
    If hasBendLen Then
        engBlock = engBlock & PadKey("length") & " - " & FmtNum(bendLenMm, iDecimalPlaces) & " mm" & vbCrLf
    Else
        engBlock = engBlock & PadKey("length") & " - —" & vbCrLf
    End If
    engBlock = engBlock & PadKey("rate") & " - " & FmtNumG(dPricePerMmBendLineEngraver) & vbCrLf
    engBlock = engBlock & PadKey("price") & " - " & FormatMoney(engCost)
    modules = modules & vbCrLf & MODULE_SEP & vbCrLf & engBlock

    ' Module 4 — settings snapshot (for restore from TXT)
    modules = modules & vbCrLf & MODULE_SEP & vbCrLf & BuildSettingsBlock()

    BuildReport = modules
End Function

Private Function BuildSettingsBlock() As String
    Dim s As String
    s = "Settings" & vbCrLf
    s = s & PadSettingsKey("decimal places") & " - " & CStr(ClampDec(iDecimalPlaces)) & vbCrLf
    s = s & PadSettingsKey("material allowance") & " - " & FmtNumG(dMaterialAllowanceMm) & " mm" & vbCrLf
    s = s & PadSettingsKey("price per bend") & " - " & FmtNumG(dPricePerBend) & vbCrLf
    s = s & PadSettingsKey("price per mm engraver") & " - " & FmtNumG(dPricePerMmBendLineEngraver) & vbCrLf
    s = s & PadSettingsKey("price per mm cut") & " - " & FmtNumG(dPricePerMmCutPer1MmThickness) & vbCrLf
    s = s & PadSettingsKey("material price") & " - " & FmtNumG(dMaterialPrice) & vbCrLf
    s = s & PadSettingsKey("material price unit") & " - " & IIf(bMaterialPricePerKg, "Per 1 kg", "Per 1 m2") & vbCrLf
    s = s & PadSettingsKey("material quantity") & " - " & IIf(bMaterialNet, "Area", "Blank")
    BuildSettingsBlock = s
End Function

Private Function PadKey(ByVal key As String) As String
    If Len(key) >= KEY_COL Then
        PadKey = key
    Else
        PadKey = key & Space$(KEY_COL - Len(key))
    End If
End Function

Private Function PadSettingsKey(ByVal key As String) As String
    If Len(key) >= SETTINGS_KEY_COL Then
        PadSettingsKey = key
    Else
        PadSettingsKey = key & Space$(SETTINGS_KEY_COL - Len(key))
    End If
End Function

' Invariant decimal point (panel uses "."); avoids locale commas in MsgBox/TXT.
Private Function FmtNum(ByVal v As Double, ByVal dec As Integer) As String
    Dim n As Integer
    Dim s As String
    n = ClampDec(dec)
    If n = 0 Then
        s = Format$(v, "0")
    Else
        s = Format$(v, "0." & String$(n, "0"))
    End If
    FmtNum = Replace(Replace(s, " ", ""), ",", ".")
End Function

Private Function FmtNumG(ByVal v As Double) As String
    Dim s As String
    s = Trim$(Str$(v)) ' Str always uses "." as decimal separator
    FmtNumG = s
End Function

Private Function FormatMoney(ByVal v As Variant) As String
    If IsEmpty(v) Or IsNull(v) Then
        FormatMoney = "(not calculated)"
    Else
        FormatMoney = FmtNum(CDbl(v), 2)
    End If
End Function

Private Function ClampDec(ByVal n As Integer) As Integer
    If n < 0 Then
        ClampDec = 0
    ElseIf n > 10 Then
        ClampDec = 10
    Else
        ClampDec = n
    End If
End Function

'==============================================================================
' PART INFO
'==============================================================================
Private Sub GetPartInfo(ByRef isSm As Boolean, ByRef partName As String, _
                        ByRef thickness As String, ByRef material As String, ByRef weight As String, _
                        ByRef hasWeight As Boolean, ByRef weightKg As Double, _
                        ByRef hasThk As Boolean, ByRef thkMm As Double)
    Dim feat As Object
    Dim smData As Object
    Dim hasFlat As Boolean
    Dim thkM As Double
    Dim hasThkM As Boolean
    Dim database As String
    Dim matName As String
    Dim ext As Object
    Dim massProp As Object
    Dim mass As Double

    isSm = False
    partName = UNDEF
    thickness = UNDEF
    material = UNDEF
    weight = UNDEF
    hasWeight = False
    weightKg = 0#
    hasThk = False
    thkMm = 0#
    hasFlat = False
    hasThkM = False

    On Error Resume Next
    partName = swModel.GetTitle
    If Len(partName) = 0 Then partName = UNDEF

    If swModel.GetType <> swDocPART Then Exit Sub

    Set feat = swModel.FirstFeature
    Do While Not feat Is Nothing
        If StrComp(feat.GetTypeName2, "FlatPattern", vbTextCompare) = 0 Then hasFlat = True
        If StrComp(feat.GetTypeName2, "SheetMetal", vbTextCompare) = 0 Then
            Set smData = feat.GetDefinition
            If Not smData Is Nothing Then
                thkM = smData.thickness
                hasThkM = True
            End If
        End If
        Set feat = feat.GetNextFeature
    Loop

    isSm = hasFlat
    If hasThkM Then
        thkMm = thkM * API_TO_MM
        hasThk = (thkMm > 0)
        thickness = FmtNum(thkMm, iDecimalPlaces) & " mm"
    End If

    matName = ""
    database = ""
    matName = swModel.GetMaterialPropertyName2("", database)
    If Len(Trim$(matName)) > 0 Then material = matName

    If material <> UNDEF Then
        Set ext = swModel.Extension
        If Not ext Is Nothing Then
            Set massProp = ext.CreateMassProperty
            If Not massProp Is Nothing Then
                mass = massProp.mass
                If Err.Number = 0 And mass >= 0 Then
                    hasWeight = True
                    weightKg = mass
                    weight = FmtNum(mass, iDecimalPlaces) & " kg"
                End If
                Err.Clear
            End If
        End If
    End If
    On Error GoTo 0
End Sub

'==============================================================================
' FLATTEN / GEOMETRY
'==============================================================================
Private Function CanFlatten(ByRef msg As String) As Boolean
    Dim flatFeat As Object
    Dim wasSuppressed As Boolean
    Dim flatBody As Object
    Dim ok As Boolean

    CanFlatten = False
    msg = ""

    If swModel.GetType <> swDocPART Then
        msg = "Active document is not a part."
        Exit Function
    End If

    Set flatFeat = GetFlatPatternFeature()
    If flatFeat Is Nothing Then
        msg = "Part is not sheet metal or has no flat pattern."
        Exit Function
    End If

    If Not TryGetSuppressed(flatFeat, wasSuppressed) Then
        msg = "Cannot check Flat Pattern state."
        Exit Function
    End If

    On Error GoTo Fail
    If wasSuppressed Then
        ok = flatFeat.SetSuppression2(swUnSuppressFeature, swThisConfiguration, Empty)
        If Not ok Then
            msg = "Cannot flatten this part: Flat-Pattern/Flatten operation is unavailable."
            Exit Function
        End If
        swModel.ForceRebuild3 False
        If Not TryGetSuppressed(flatFeat, ok) Or ok Then
            FoldBack flatFeat, wasSuppressed
            msg = "Cannot flatten this part: Flat-Pattern stayed folded after flatten command."
            Exit Function
        End If
    End If

    Set flatBody = GetFlatPatternBody()
    FoldBack flatFeat, wasSuppressed

    If flatBody Is Nothing Then
        msg = "Cannot flatten this part: Flat pattern body is unavailable."
        Exit Function
    End If

    CanFlatten = True
    msg = "Flat pattern is available."
    Exit Function

Fail:
    FoldBack flatFeat, wasSuppressed
    msg = "Cannot flatten this part: an error occurred while preparing Flat Pattern."
    CanFlatten = False
End Function

Private Function CalcOnFlatPattern(ByRef areaMm2 As Double, ByRef hasArea As Boolean, _
                                   ByRef blankL As Double, ByRef blankW As Double, ByRef blankA As Double, ByRef hasBlank As Boolean, _
                                   ByRef laserMm As Double, ByRef hasLaser As Boolean, _
                                   ByRef bendCnt As Long, ByRef hasBendCnt As Boolean, _
                                   ByRef bendLenMm As Double, ByRef hasBendLen As Boolean, _
                                   ByRef hasOverlap As Boolean, ByRef overlapMsg As String) As Boolean
    Dim flatFeat As Object
    Dim wasSuppressed As Boolean
    Dim flatBody As Object
    Dim ok As Boolean
    Dim checkRes As Long

    CalcOnFlatPattern = False
    hasArea = False: hasBlank = False: hasLaser = False
    hasBendCnt = False: hasBendLen = False
    hasOverlap = False
    overlapMsg = ""

    Set flatFeat = GetFlatPatternFeature()
    If flatFeat Is Nothing Then Exit Function
    If Not TryGetSuppressed(flatFeat, wasSuppressed) Then Exit Function

    On Error GoTo Fail
    If wasSuppressed Then
        ok = flatFeat.SetSuppression2(swUnSuppressFeature, swThisConfiguration, Empty)
        If Not ok Then Exit Function
        swModel.ForceRebuild3 False
        If Not TryGetSuppressed(flatFeat, ok) Or ok Then
            FoldBack flatFeat, wasSuppressed
            Exit Function
        End If
    End If

    Set flatBody = GetFlatPatternBody()
    If flatBody Is Nothing Then
        FoldBack flatFeat, wasSuppressed
        Exit Function
    End If

    ' Tools > Evaluate > Check equivalent
    On Error Resume Next
    Err.Clear
    checkRes = flatBody.Check2
    If Err.Number <> 0 Then
        Err.Clear
        overlapMsg = "Check could not be run."
        hasOverlap = False
    ElseIf checkRes <> 0 Then
        hasOverlap = True
        overlapMsg = "Geometry check found issues (Tools > Evaluate > Check). Fix errors in the flat pattern."
        FoldBack flatFeat, wasSuppressed
        CalcOnFlatPattern = True
        Exit Function
    Else
        hasOverlap = False
        overlapMsg = "No overlaps detected in the flat sheet (Check passed)."
    End If
    On Error GoTo Fail

    hasArea = TryPlanarAreaMm2(flatBody, areaMm2)
    hasBlank = TryBlankArea(flatBody, blankL, blankW, blankA)
    hasLaser = TryLaserPathMm(flatBody, laserMm)
    hasBendCnt = True
    bendCnt = GetBendLinesCount()
    hasBendLen = True
    bendLenMm = GetBendLinesLengthMm(IIf(hasLaser, laserMm, -1#), hasLaser)

    FoldBack flatFeat, wasSuppressed
    CalcOnFlatPattern = True
    Exit Function

Fail:
    FoldBack flatFeat, wasSuppressed
    CalcOnFlatPattern = False
End Function

Private Sub FoldBack(ByVal flatFeat As Object, ByVal wasSuppressed As Boolean)
    On Error Resume Next
    If wasSuppressed And Not flatFeat Is Nothing Then
        flatFeat.SetSuppression2 swSuppressFeature, swThisConfiguration, Empty
    End If
    Err.Clear
End Sub

Private Function TryGetSuppressed(ByVal feat As Object, ByRef isSuppressed As Boolean) As Boolean
    Dim v As Variant
    On Error Resume Next
    Err.Clear
    v = feat.IsSuppressed2(swThisConfiguration, Empty)
    If Err.Number <> 0 Then
        Err.Clear
        TryGetSuppressed = False
        Exit Function
    End If
    If IsArray(v) Then
        isSuppressed = CBool(v(0))
        TryGetSuppressed = True
    Else
        isSuppressed = CBool(v)
        TryGetSuppressed = True
    End If
End Function

Private Function GetFlatPatternFeature() As Object
    Dim feat As Object
    Set GetFlatPatternFeature = Nothing
    On Error Resume Next
    Set feat = swModel.FirstFeature
    Do While Not feat Is Nothing
        If StrComp(feat.GetTypeName2, "FlatPattern", vbTextCompare) = 0 Then
            Set GetFlatPatternFeature = feat
            Exit Function
        End If
        Set feat = feat.GetNextFeature
    Loop
End Function

Private Function GetFlatPatternBody() As Object
    Dim feat As Object
    Dim fpData As Object
    Dim fixedFace As Object
    Set GetFlatPatternBody = Nothing
    On Error Resume Next
    Set feat = GetFlatPatternFeature()
    If feat Is Nothing Then Exit Function
    Set fpData = feat.GetDefinition
    If fpData Is Nothing Then Exit Function
    Set fixedFace = fpData.FixedFace2
    If fixedFace Is Nothing Then Exit Function
    Set GetFlatPatternBody = fixedFace.GetBody
End Function

Private Function TryPlanarAreaMm2(ByVal body As Object, ByRef areaMm2 As Double) As Boolean
    Dim faces As Variant
    Dim i As Long
    Dim face As Object
    Dim a As Double, maxA As Double

    TryPlanarAreaMm2 = False
    On Error Resume Next
    faces = body.GetFaces
    If Err.Number <> 0 Or IsEmpty(faces) Then
        Err.Clear
        Exit Function
    End If
    maxA = 0#
    For i = 0 To UBound(faces)
        Set face = faces(i)
        If Not face Is Nothing Then
            a = face.GetArea
            If a > maxA Then maxA = a
        End If
    Next i
    If maxA <= 0 Then Exit Function
    areaMm2 = maxA * 1000000#
    TryPlanarAreaMm2 = True
End Function

Private Function TryBlankArea(ByVal body As Object, ByRef lengthMm As Double, _
                              ByRef widthMm As Double, ByRef areaMm2 As Double) As Boolean
    Dim boxObj As Variant
    Dim box() As Double
    Dim dx As Double, dy As Double, dz As Double
    Dim d(0 To 2) As Double
    Dim allow As Double

    TryBlankArea = False
    allow = dMaterialAllowanceMm
    On Error Resume Next
    boxObj = body.GetBodyBox
    If Err.Number <> 0 Or IsEmpty(boxObj) Then
        Err.Clear
        Exit Function
    End If
    If Not IsArray(boxObj) Then Exit Function
    If UBound(boxObj) < 5 Then Exit Function

    dx = (CDbl(boxObj(3)) - CDbl(boxObj(0))) * API_TO_MM
    dy = (CDbl(boxObj(4)) - CDbl(boxObj(1))) * API_TO_MM
    dz = (CDbl(boxObj(5)) - CDbl(boxObj(2))) * API_TO_MM

    If dz > dx Or dz > dy Then
        d(0) = dx: d(1) = dy: d(2) = dz
        Sort3 d
        lengthMm = d(2) + 2# * allow
        widthMm = d(1) + 2# * allow
    Else
        If dx >= dy Then
            lengthMm = dx + 2# * allow
            widthMm = dy + 2# * allow
        Else
            lengthMm = dy + 2# * allow
            widthMm = dx + 2# * allow
        End If
    End If
    areaMm2 = lengthMm * widthMm
    TryBlankArea = True
End Function

Private Sub Sort3(ByRef d() As Double)
    Dim t As Double
    If d(0) > d(1) Then t = d(0): d(0) = d(1): d(1) = t
    If d(1) > d(2) Then t = d(1): d(1) = d(2): d(2) = t
    If d(0) > d(1) Then t = d(0): d(0) = d(1): d(1) = t
End Sub

Private Function TryLaserPathMm(ByVal flatBody As Object, ByRef laserMm As Double) As Boolean
    Dim mainFace As Object
    Dim selMgr As Object
    Dim selData As Object
    Dim ext As Object
    Dim measure As Object
    Dim ok As Boolean
    Dim periM As Double
    Dim ent As Object

    TryLaserPathMm = False
    Set mainFace = GetMaxAreaFace(flatBody)
    If mainFace Is Nothing Then Exit Function

    On Error Resume Next
    swModel.ClearSelection2 True
    Set selMgr = swModel.SelectionManager
    If selMgr Is Nothing Then Exit Function
    Set selData = selMgr.CreateSelectData
    Set ent = mainFace
    ok = ent.Select4(False, selData)
    If Not ok Then
        swModel.ClearSelection2 True
        Exit Function
    End If

    Set ext = swModel.Extension
    If ext Is Nothing Then
        swModel.ClearSelection2 True
        Exit Function
    End If
    Set measure = ext.CreateMeasure
    If measure Is Nothing Then
        swModel.ClearSelection2 True
        Exit Function
    End If

    Err.Clear
    ok = measure.Calculate(Array(mainFace))
    swModel.ClearSelection2 True
    If Err.Number <> 0 Or Not ok Then
        Err.Clear
        Exit Function
    End If
    periM = measure.Perimeter
    If periM <= 0 Then Exit Function
    laserMm = periM * API_TO_MM
    TryLaserPathMm = True
End Function

Private Function GetMaxAreaFace(ByVal body As Object) As Object
    Dim faces As Variant
    Dim i As Long
    Dim face As Object
    Dim a As Double, maxA As Double
    Dim best As Object

    Set GetMaxAreaFace = Nothing
    On Error Resume Next
    faces = body.GetFaces
    If IsEmpty(faces) Then Exit Function
    maxA = 0#
    For i = 0 To UBound(faces)
        Set face = faces(i)
        If Not face Is Nothing Then
            a = face.GetArea
            If a > maxA Then
                maxA = a
                Set best = face
            End If
        End If
    Next i
    Set GetMaxAreaFace = best
End Function

'==============================================================================
' BEND LINES (same rules as SheetMetalCalcService)
'==============================================================================
Private Function GetBendLinesCount() As Long
    Dim flatFeat As Object
    Dim subFeat As Object
    Dim sketch As Object
    Dim total As Long
    Dim feat As Object
    Dim adjusted As Long

    total = 0
    On Error Resume Next
    Set flatFeat = GetFlatPatternFeature()
    If Not flatFeat Is Nothing Then
        Set subFeat = flatFeat.GetFirstSubFeature
        Do While Not subFeat Is Nothing
            Set sketch = Nothing
            Set sketch = subFeat.GetSpecificFeature2
            ' Late binding: any object with GetSketchSegments is treated as a sketch
            If Not sketch Is Nothing Then
                total = total + CountBendGroupsInSketch(sketch)
            End If
            Set subFeat = subFeat.GetNextSubFeature
        Loop
    End If

    If total = 0 Then
        Set feat = swModel.FirstFeature
        Do While Not feat Is Nothing
            If StrComp(feat.GetTypeName2, "SM3dBend", vbTextCompare) = 0 Then
                total = total + CountSketchedBend(feat)
            End If
            Set feat = feat.GetNextFeature
        Loop
    End If

    ' Panel subtracts 4 helper construction segments typical for flat-pattern sketches
    adjusted = total - 4
    If adjusted < 0 Then adjusted = 0
    GetBendLinesCount = adjusted
End Function

Private Function GetBendLinesLengthMm(ByVal laserMm As Double, ByVal hasLaser As Boolean) As Double
    Dim flatFeat As Object
    Dim subFeat As Object
    Dim sketch As Object
    Dim totalM As Double
    Dim feat As Object
    Dim totalMm As Double

    totalM = 0#
    On Error Resume Next
    Set flatFeat = GetFlatPatternFeature()
    If Not flatFeat Is Nothing Then
        Set subFeat = flatFeat.GetFirstSubFeature
        Do While Not subFeat Is Nothing
            Set sketch = subFeat.GetSpecificFeature2
            If Not sketch Is Nothing Then
                totalM = totalM + SketchConstructionLengthM(sketch)
            End If
            Set subFeat = subFeat.GetNextSubFeature
        Loop
    End If

    If totalM <= 0# Then
        Set feat = swModel.FirstFeature
        Do While Not feat Is Nothing
            If StrComp(feat.GetTypeName2, "SM3dBend", vbTextCompare) = 0 Then
                totalM = totalM + SketchedBendLengthM(feat)
            End If
            Set feat = feat.GetNextFeature
        Loop
    End If

    If totalM <= 0# Then
        GetBendLinesLengthMm = 0#
        Exit Function
    End If

    totalMm = totalM * API_TO_MM
    ' Subtract perimeter if construction sketches also contain cut contour
    If hasLaser And laserMm > 0 And totalMm > laserMm Then
        totalMm = totalMm - laserMm
    End If
    If totalMm < 0 Then totalMm = 0#
    GetBendLinesLengthMm = totalMm
End Function

Private Function CountSketchedBend(ByVal bendFeat As Object) As Long
    Dim subFeat As Object
    Dim sketch As Object
    CountSketchedBend = 0
    On Error Resume Next
    Set subFeat = bendFeat.GetFirstSubFeature
    Do While Not subFeat Is Nothing
        If StrComp(subFeat.GetTypeName2, "ProfileFeature", vbTextCompare) = 0 Then
            Set sketch = subFeat.GetSpecificFeature2
            If Not sketch Is Nothing Then
                CountSketchedBend = CountBendGroupsInSketch(sketch)
                Exit Function
            End If
        End If
        Set subFeat = subFeat.GetNextSubFeature
    Loop
End Function

Private Function SketchedBendLengthM(ByVal bendFeat As Object) As Double
    Dim subFeat As Object
    Dim sketch As Object
    SketchedBendLengthM = 0#
    On Error Resume Next
    Set subFeat = bendFeat.GetFirstSubFeature
    Do While Not subFeat Is Nothing
        If StrComp(subFeat.GetTypeName2, "ProfileFeature", vbTextCompare) = 0 Then
            Set sketch = subFeat.GetSpecificFeature2
            If Not sketch Is Nothing Then
                SketchedBendLengthM = SketchConstructionLengthM(sketch)
                Exit Function
            End If
        End If
        Set subFeat = subFeat.GetNextSubFeature
    Loop
End Function

Private Function SketchConstructionLengthM(ByVal sketch As Object) As Double
    Dim segs As Variant
    Dim i As Long
    Dim seg As Object
    Dim line As Object
    Dim sp As Object, ep As Object
    Dim dx As Double, dy As Double, dz As Double, ln As Double
    Dim sum As Double

    sum = 0#
    On Error Resume Next
    segs = sketch.GetSketchSegments
    If IsEmpty(segs) Then
        SketchConstructionLengthM = 0#
        Exit Function
    End If
    For i = 0 To UBound(segs)
        Set seg = segs(i)
        If Not seg Is Nothing Then
            If seg.GetType = swSketchLINE And seg.ConstructionGeometry Then
                Set line = seg
                Set sp = line.GetStartPoint2
                Set ep = line.GetEndPoint2
                If Not sp Is Nothing And Not ep Is Nothing Then
                    dx = ep.X - sp.X
                    dy = ep.Y - sp.Y
                    dz = ep.Z - sp.Z
                    ln = Sqr(dx * dx + dy * dy + dz * dz)
                    If ln > 0 Then sum = sum + ln
                End If
            End If
        End If
    Next i
    SketchConstructionLengthM = sum
End Function

' Group colinear connected construction lines into bend counts (BFS)
Private Function CountBendGroupsInSketch(ByVal sketch As Object) As Long
    Dim segs As Variant
    Dim n As Long, i As Long, j As Long, k As Long
    Dim seg As Object, line As Object, sp As Object, ep As Object
    Dim isLine() As Boolean
    Dim p1x() As Double, p1y() As Double, p1z() As Double
    Dim p2x() As Double, p2y() As Double, p2z() As Double
    Dim dx() As Double, dy() As Double, dz() As Double
    Dim visited() As Boolean
    Dim queue() As Long
    Dim qHead As Long, qTail As Long
    Dim groups As Long
    Dim vx As Double, vy As Double, vz As Double, ln As Double
    Dim dot As Double
    Dim tol As Double, cosTol As Double

    CountBendGroupsInSketch = 0
    tol = 0.000001
    cosTol = 0.9999

    On Error Resume Next
    segs = sketch.GetSketchSegments
    If IsEmpty(segs) Then Exit Function
    n = UBound(segs) - LBound(segs) + 1
    If n <= 0 Then Exit Function

    ReDim isLine(0 To n - 1)
    ReDim p1x(0 To n - 1): ReDim p1y(0 To n - 1): ReDim p1z(0 To n - 1)
    ReDim p2x(0 To n - 1): ReDim p2y(0 To n - 1): ReDim p2z(0 To n - 1)
    ReDim dx(0 To n - 1): ReDim dy(0 To n - 1): ReDim dz(0 To n - 1)
    ReDim visited(0 To n - 1)
    ReDim queue(0 To n - 1)

    For i = 0 To n - 1
        Set seg = segs(LBound(segs) + i)
        isLine(i) = False
        If Not seg Is Nothing Then
            If seg.GetType = swSketchLINE And seg.ConstructionGeometry Then
                Set line = seg
                Set sp = line.GetStartPoint2
                Set ep = line.GetEndPoint2
                If Not sp Is Nothing And Not ep Is Nothing Then
                    p1x(i) = sp.X: p1y(i) = sp.Y: p1z(i) = sp.Z
                    p2x(i) = ep.X: p2y(i) = ep.Y: p2z(i) = ep.Z
                    vx = p2x(i) - p1x(i)
                    vy = p2y(i) - p1y(i)
                    vz = p2z(i) - p1z(i)
                    ln = Sqr(vx * vx + vy * vy + vz * vz)
                    If ln > tol Then
                        dx(i) = vx / ln
                        dy(i) = vy / ln
                        dz(i) = vz / ln
                        isLine(i) = True
                    End If
                End If
            End If
        End If
    Next i

    groups = 0
    For i = 0 To n - 1
        If isLine(i) And Not visited(i) Then
            groups = groups + 1
            qHead = 0: qTail = 0
            visited(i) = True
            queue(qTail) = i: qTail = qTail + 1
            Do While qHead < qTail
                j = queue(qHead): qHead = qHead + 1
                For k = 0 To n - 1
                    If isLine(k) And Not visited(k) Then
                        If ShareEndpoint(p1x(j), p1y(j), p1z(j), p2x(j), p2y(j), p2z(j), _
                                         p1x(k), p1y(k), p1z(k), p2x(k), p2y(k), p2z(k), tol) Then
                            dot = dx(j) * dx(k) + dy(j) * dy(k) + dz(j) * dz(k)
                            If Abs(dot) >= cosTol Then
                                visited(k) = True
                                queue(qTail) = k: qTail = qTail + 1
                            End If
                        End If
                    End If
                Next k
            Loop
        End If
    Next i

    CountBendGroupsInSketch = groups
End Function

Private Function ShareEndpoint(ByVal a1x As Double, ByVal a1y As Double, ByVal a1z As Double, _
                               ByVal a2x As Double, ByVal a2y As Double, ByVal a2z As Double, _
                               ByVal b1x As Double, ByVal b1y As Double, ByVal b1z As Double, _
                               ByVal b2x As Double, ByVal b2y As Double, ByVal b2z As Double, _
                               ByVal tol As Double) As Boolean
    Dim t2 As Double
    t2 = tol * tol
    ShareEndpoint = (Dist2(a1x, a1y, a1z, b1x, b1y, b1z) <= t2) Or _
                    (Dist2(a1x, a1y, a1z, b2x, b2y, b2z) <= t2) Or _
                    (Dist2(a2x, a2y, a2z, b1x, b1y, b1z) <= t2) Or _
                    (Dist2(a2x, a2y, a2z, b2x, b2y, b2z) <= t2)
End Function

Private Function Dist2(ByVal x1 As Double, ByVal y1 As Double, ByVal z1 As Double, _
                       ByVal x2 As Double, ByVal y2 As Double, ByVal z2 As Double) As Double
    Dim dx As Double, dy As Double, dz As Double
    dx = x1 - x2: dy = y1 - y2: dz = z1 - z2
    Dist2 = dx * dx + dy * dy + dz * dz
End Function

'==============================================================================
' PRICING (same formulas as ExportTaskPaneControl)
'==============================================================================
Private Function CalcMaterialCost(ByVal hasWeight As Boolean, ByVal weightKg As Double, _
                                  ByVal hasArea As Boolean, ByVal areaMm2 As Double, _
                                  ByVal hasBlank As Boolean, ByVal blankAreaMm2 As Double, _
                                  ByVal hasThk As Boolean, ByVal thkMm As Double) As Variant
    Dim blankMass As Double
    CalcMaterialCost = Empty

    If bMaterialNet Then
        If bMaterialPricePerKg Then
            If hasWeight And weightKg >= 0 Then CalcMaterialCost = weightKg * dMaterialPrice
        Else
            If hasArea And areaMm2 > 0 Then CalcMaterialCost = (areaMm2 / 1000000#) * dMaterialPrice
        End If
    Else
        If bMaterialPricePerKg Then
            If hasBlank And blankAreaMm2 > 0 And hasArea And areaMm2 > 0 And hasWeight And weightKg >= 0 Then
                blankMass = weightKg * (blankAreaMm2 / areaMm2)
                CalcMaterialCost = blankMass * dMaterialPrice
            End If
        Else
            If hasBlank And blankAreaMm2 > 0 Then CalcMaterialCost = (blankAreaMm2 / 1000000#) * dMaterialPrice
        End If
    End If
End Function

Private Function CalcCutCost(ByVal hasLaser As Boolean, ByVal laserMm As Double, _
                             ByVal hasThk As Boolean, ByVal thkMm As Double) As Variant
    CalcCutCost = Empty
    If Not hasLaser Or laserMm <= 0 Then Exit Function
    If Not hasThk Or thkMm <= 0 Then Exit Function
    If dPricePerMmCutPer1MmThickness <= 0 Then Exit Function
    CalcCutCost = laserMm * thkMm * dPricePerMmCutPer1MmThickness
End Function

Private Function CalcBendCost(ByVal hasCnt As Boolean, ByVal cnt As Long) As Variant
    CalcBendCost = Empty
    If Not hasCnt Or cnt <= 0 Then Exit Function
    If dPricePerBend <= 0 Then Exit Function
    CalcBendCost = CDbl(cnt) * dPricePerBend
End Function

Private Function CalcEngraverCost(ByVal hasLen As Boolean, ByVal lenMm As Double) As Variant
    CalcEngraverCost = Empty
    If Not hasLen Or lenMm <= 0 Then Exit Function
    If dPricePerMmBendLineEngraver <= 0 Then Exit Function
    CalcEngraverCost = lenMm * dPricePerMmBendLineEngraver
End Function

Private Function FormatTotalPrice(ByVal matCost As Variant, ByVal cutCost As Variant, _
                                  ByVal bendCost As Variant, ByVal engCost As Variant) As String
    Dim total As Double
    Dim workOnly As Double

    workOnly = 0#
    If Not IsEmpty(cutCost) Then workOnly = workOnly + CDbl(cutCost)
    If Not IsEmpty(bendCost) Then workOnly = workOnly + CDbl(bendCost)
    If Not IsEmpty(engCost) Then workOnly = workOnly + CDbl(engCost)

    If IsEmpty(matCost) Then
        FormatTotalPrice = FmtNum(workOnly, 2) & " (work price, material excluded)"
        Exit Function
    End If

    total = CDbl(matCost) + workOnly
    FormatTotalPrice = FmtNum(total, 2)
End Function

'==============================================================================
' TXT EXPORT
'==============================================================================
Private Function CanWriteNextToModel() As Boolean
    Dim p As String
    On Error Resume Next
    p = swModel.GetPathName
    CanWriteNextToModel = (Len(p) > 0)
End Function

Private Function BuildTxtPath() As String
    Dim docPath As String
    Dim dir As String, baseName As String
    Dim material As String, thickness As String
    Dim stamp As String
    Dim isSm As Boolean, partName As String, thk As String, mat As String, w As String
    Dim hasW As Boolean, wKg As Double, hasT As Boolean, tMm As Double

    BuildTxtPath = ""
    On Error Resume Next
    docPath = swModel.GetPathName
    If Len(docPath) = 0 Then Exit Function

    GetPartInfo isSm, partName, thk, mat, w, hasW, wKg, hasT, tMm
    material = SafeFilePart(mat)
    thickness = SafeFilePart(thk)

    dir = Left$(docPath, InStrRev(docPath, "\") - 1)
    baseName = Mid$(docPath, InStrRev(docPath, "\") + 1)
    If InStrRev(baseName, ".") > 0 Then baseName = Left$(baseName, InStrRev(baseName, ".") - 1)

    stamp = Format$(Now, "yyyy-mm-dd_hh-nn-ss")
    BuildTxtPath = dir & "\" & baseName & "_" & material & "_" & thickness & "_" & stamp & ".txt"
End Function

Private Function SafeFilePart(ByVal value As String) As String
    Dim s As String
    Dim i As Long
    Dim ch As String
    Dim bad As String

    If Len(Trim$(value)) = 0 Or value = UNDEF Then
        SafeFilePart = "unknown"
        Exit Function
    End If
    s = Trim$(value)
    bad = "\:*?""<>|/"
    For i = 1 To Len(bad)
        ch = Mid$(bad, i, 1)
        s = Replace(s, ch, "_")
    Next i
    s = Replace(s, " ", "_")
    Do While InStr(s, "__") > 0
        s = Replace(s, "__", "_")
    Loop
    If Len(s) = 0 Then s = "unknown"
    SafeFilePart = s
End Function

Private Function WriteTextFile(ByVal path As String, ByVal content As String) As Boolean
    Dim fso As Object
    Dim ts As Object
    On Error GoTo Fail
    Set fso = CreateObject("Scripting.FileSystemObject")
    Set ts = fso.CreateTextFile(path, True, True) ' Unicode
    ts.Write content
    ts.Close
    WriteTextFile = True
    Exit Function
Fail:
    WriteTextFile = False
End Function

SolidWorks · VBA · 2 дня назад · обновлено сегодня

Экспорт файла (PDF, DXF, STL, CSV)

Этот макрос позволяет записывать одним кликом файл в разных форматах - PDF, STL, DXF и таблицы в Exel (CSV). Типы экспортируемых форматов и способы работы можно регулировать в настройках. Старые файлы можно переносятся в папку OLD.

VBA
'==============================================================================
' ExportTools.bas — SolidWorks VBA export macro (single file)
'
' WHAT THIS MACRO DOES
'   Exports the active document next to the model file:
'     PDF, DXF (sheet-metal flat / drawing), STL, Excel (drawing tables).
'   Before overwriting an existing export, the old file is moved/copied into
'   a subfolder named "old\" with a timestamp from DateLastModified.
'
' HOW TO USE
'   1) Edit the SETTINGS constants below (True/False and mode numbers).
'   2) In SolidWorks: Tools > Macro > Run > select this .bas (or assign to a button).
'   3) The active document must already be saved on disk (needs a real path).
'
' RETURN CODES (each Export* function)
'   0 = success (OK)
'   1 = failure (FAIL)
'
' TYPICAL FLOW FOR ONE FORMAT
'   BuildExportPath > ArchiveToOld (if file exists) > write new file
'
' NOTES FOR BEGINNERS
'   - "Private" = only visible inside this module; "Public" = callable from outside.
'   - "Const" = constant value you change at the top; do not change SW API constants.
'   - Late binding: we use Object + CreateObject/GetObject (no SW type library required).
'   - On Error Resume Next = ignore errors for the next statements (used around SW/Excel calls).
'==============================================================================

Option Explicit

'----- SETTINGS (edit these) --------------------------------------------------
' Turn each export mode on/off. Only enabled modes run from main().

Private Const bExportPdf As Boolean = True
' True  = export PDF of the active document (any type that SaveAs supports).

Private Const bExportDxf As Boolean = True
' True  = export DXF.
' Allowed only for: sheet-metal Part (flat pattern) OR Drawing.

Private Const bExportStl As Boolean = True
' True  = export STL.
' Allowed only for: Part OR Assembly (not Drawing).

Private Const bExportExcel As Boolean = True
' True  = dump drawing tables into an .xlsx file.
' Allowed only for: Drawing that contains at least one table.

Private Const iStlAssemblyMode As Integer = 3
' How to export STL when the active document is an Assembly:
'   1 = one combined STL file (assembly as a single mesh)
'   2 = separate STL per component (SolidWorks naming: "Asm - Comp^Asm-1.STL")
'   3 = both (1 then 2)
' Ignored for Parts (always one file).

Private Const bDxfAddThickness As Boolean = True
' True  = for sheet-metal Part DXF, append thickness to the file name:
'         e.g. "Bracket (S 2 mm).dxf"
' Thickness is read from the SheetMetal feature (meters > mm).

Private Const bRemoveSpaces As Boolean = True
' True  = normalize EVERY export file name:
'         spaces > underscore "_", and entire name to lowercase.
'         Example: "My Part (S 2 mm).dxf" > "my_part_(s_2_mm).dxf"
' Also renames per-component STL files after SolidWorks creates them.

Private Const iNotify As Integer = 1
' 0 = show MsgBox only when something failed
' 1 = always show a summary MsgBox (OK/FAIL per mode)

Private Const swDocPART As Long = 1
Private Const swDocASSEMBLY As Long = 2
Private Const swDocDRAWING As Long = 3
Private Const swSTLComponentsIntoOneFile As Long = 72

Private Const swDxfOutputNoScale As Long = 136

Private swApp As Object
Private swModel As Object
Private mBase As String
Private mFolder As String

'===== ENTRY ==================================================================

Public Sub main()
    Dim Fail As Integer, msg As String

    On Error Resume Next
    Set swApp = GetObject(, "SldWorks.Application")
    On Error GoTo 0
    If swApp Is Nothing Then MsgErr "Cannot connect to SolidWorks.": Exit Sub

    Set swModel = swApp.ActiveDoc
    If swModel Is Nothing Then MsgErr "No active document.": Exit Sub

    If Len(swModel.GetPathName) = 0 Then MsgErr "Document must be saved before export.": Exit Sub

    With fso()
        mBase = .GetBaseName(swModel.GetPathName)
        mFolder = .GetParentFolderName(swModel.GetPathName)
    End With

    Fail = 0: msg = ""
    If bExportPdf Then Acc "PDF", ExportPdf(), Fail, msg
    If bExportDxf And CanExportDxf() Then Acc "DXF", ExportDxf(), Fail, msg
    If bExportStl And CanExportStl() Then Acc "STL", ExportStl(), Fail, msg
    If bExportExcel And CanExportExcel() Then Acc "Excel", ExportExcel(), Fail, msg

    If Len(msg) = 0 Then msg = "No export modes enabled.": Fail = 1
    If iNotify = 1 Or Fail <> 0 Then MsgBox Trim$(msg), IIf(Fail = 0, vbInformation, vbExclamation), "ExportTools"
End Sub

Private Sub Acc(ByVal label As String, ByVal code As Integer, ByRef Fail As Integer, ByRef msg As String)
    Fail = Fail Or code
    msg = msg & label & ": " & IIf(code = 0, "OK", "FAIL") & vbCrLf
End Sub

'===== EXPORT MODES ===========================================================

Public Function ExportPdf() As Integer
    Dim path As String: ExportPdf = ExportSaveAs("pdf", path)
End Function

Public Function ExportDxf() As Integer
    Dim path As String, prevScale As Long, suffix As String, thk As Double
    If Not CanExportDxf() Then ExportDxf = 1: Exit Function

    If bDxfAddThickness And DocType() = swDocPART Then
        thk = GetSheetMetalThicknessMm()
        If thk > 0 Then suffix = " (S " & Format$(thk, "0.##") & " mm)"
    End If

    If Not BeginExport("dxf", path, suffix) Then ExportDxf = 1: Exit Function

    prevScale = PrefInt(swDxfOutputNoScale): SetPrefInt swDxfOutputNoScale, 1
    If DocType() = swDocPART Then
        ExportDxf = ExportSheetMetalDxf(path)
    Else
        ExportDxf = OkFail(SaveAsSilent(path))
    End If
    SetPrefInt swDxfOutputNoScale, prevScale
End Function

Public Function ExportStl() As Integer
    Dim path As String, prevOneFile As Boolean, Fail As Integer
    If Not CanExportStl() Then ExportStl = 1: Exit Function

    If DocType() = swDocPART Then ExportStl = ExportSaveAs("stl", path): Exit Function

    path = BuildExportPath("stl", "")
    prevOneFile = PrefToggle(swSTLComponentsIntoOneFile)
    Select Case iStlAssemblyMode
        Case 1: Fail = StlAssemblyPass(path, True)
        Case 2: Fail = StlAssemblyPass(path, False)
        Case 3: Fail = StlAssemblyPass(path, True) Or StlAssemblyPass(path, False)
        Case Else: Fail = 1
    End Select
    SetPrefToggle swSTLComponentsIntoOneFile, prevOneFile
    ExportStl = Fail
End Function

Public Function ExportExcel() As Integer
    Dim path As String, xl As Object, wb As Object, ws As Object
    Dim v As Object, tbls As Variant, ti As Long, tbl As Object
    Dim rr As Long, row As Long, c As Long, code As Integer
    On Error GoTo Fail
    If Not CanExportExcel() Then ExportExcel = 1: Exit Function
    If Not BeginExport("xlsx", path) Then ExportExcel = 1: Exit Function

    Set xl = CreateObject("Excel.Application"): xl.DisplayAlerts = False
    Set wb = xl.Workbooks.Add: Set ws = wb.Worksheets(1): ws.name = "Tables": row = 1

    Set v = swModel.GetFirstView
    Do While Not v Is Nothing
        If v.GetTableAnnotationCount > 0 Then
            tbls = v.GetTableAnnotations
            If IsArray(tbls) Then
                For ti = LBound(tbls) To UBound(tbls)
                    Set tbl = tbls(ti)
                    If Not tbl Is Nothing Then

                        For rr = 0 To tbl.RowCount - 1
                            For c = 0 To tbl.ColumnCount - 1
                                ws.Cells(row, c + 1).value = TableCellText(tbl, rr, c)
                            Next c
                            row = row + 1
                        Next rr
                        row = row + 1
                    End If
                Next ti
            End If
        End If
        Set v = v.GetNextView
    Loop

    If row > 1 Then wb.SaveAs path, 51: code = 0 Else code = 1
    ReleaseExcel xl, wb
    ExportExcel = code
    Exit Function
Fail:

    ReleaseExcel xl, wb
    NotifyFileLocked path
    ExportExcel = 1
End Function

'===== RULES (when is each mode allowed?) =====================================

Private Function DocType() As Long
    DocType = swModel.GetType
End Function

Private Function CanExportDxf() As Boolean
    Dim t As Long: t = DocType()
    CanExportDxf = (t = swDocDRAWING) Or (t = swDocPART And IsSheetMetalPart())
End Function

Private Function CanExportStl() As Boolean
    Dim t As Long: t = DocType()
    CanExportStl = (t = swDocPART Or t = swDocASSEMBLY)
End Function

Private Function CanExportExcel() As Boolean
    CanExportExcel = (DocType() = swDocDRAWING) And DrawingHasTables()
End Function

Private Function DrawingHasTables() As Boolean
    Dim v As Object
    On Error Resume Next
    Set v = swModel.GetFirstView
    Do While Not v Is Nothing
        If v.GetTableAnnotationCount > 0 Then DrawingHasTables = True: Exit Function
        Set v = v.GetNextView
    Loop
End Function

Private Function IsSheetMetalPart() As Boolean
    IsSheetMetalPart = Not FindFeatureInTypes("SheetMetal", "FlatPattern", "SheetMetalFolder") Is Nothing
End Function

Private Function FindFeatureInTypes(ParamArray typeNames() As Variant) As Object
    Dim feat As Object, i As Long, tn As String
    On Error Resume Next
    Set feat = swModel.FirstFeature
    Do While Not feat Is Nothing
        tn = feat.GetTypeName2
        For i = LBound(typeNames) To UBound(typeNames)
            If StrComp(tn, CStr(typeNames(i)), vbTextCompare) = 0 Then
                Set FindFeatureInTypes = feat: Exit Function
            End If
        Next i
        Set feat = feat.GetNextFeature
    Loop
End Function

Private Function GetSheetMetalThicknessMm() As Double
    Dim feat As Object
    On Error Resume Next
    Set feat = FindFeatureInTypes("SheetMetal")
    If Not feat Is Nothing Then GetSheetMetalThicknessMm = feat.GetDefinition.thickness * 1000#
End Function

Private Function TableCellText(ByVal tbl As Object, ByVal r As Long, ByVal c As Long) As String
    Dim s As String
    On Error Resume Next
    Err.Clear: s = tbl.GetEntryText(r, c)
    If Err.Number <> 0 Or Len(s) = 0 Then Err.Clear: s = tbl.text(r, c)
    If Err.Number <> 0 Then s = ""
    TableCellText = StripHtml(s)
End Function

Private Function StripHtml(ByVal s As String) As String
    Dim i As Long, ch As String, inTag As Boolean, out As String
    For i = 1 To Len(s)
        ch = Mid$(s, i, 1)
        If ch = "<" Then
            inTag = True
        ElseIf ch = ">" Then
            inTag = False
        ElseIf Not inTag Then
            out = out & ch
        End If
    Next i
    StripHtml = Trim$(out)
End Function

'===== PATH / WRITE HELPERS ===================================================

Private Function fso() As Object
    Static o As Object
    If o Is Nothing Then Set o = CreateObject("Scripting.FileSystemObject")
    Set fso = o
End Function

Private Function PrefInt(ByVal id As Long) As Long
    On Error Resume Next: PrefInt = swApp.GetUserPreferenceIntegerValue(id)
End Function

Private Sub SetPrefInt(ByVal id As Long, ByVal value As Long)
    On Error Resume Next: swApp.SetUserPreferenceIntegerValue id, value
End Sub

Private Function PrefToggle(ByVal id As Long) As Boolean
    On Error Resume Next: PrefToggle = swApp.GetUserPreferenceToggle(id)
End Function

Private Sub SetPrefToggle(ByVal id As Long, ByVal value As Boolean)
    On Error Resume Next: swApp.SetUserPreferenceToggle id, value
End Sub

Private Function OkFail(ByVal ok As Boolean) As Integer
    OkFail = IIf(ok, 0, 1)
End Function

Private Function BeginExport(ByVal ext As String, ByRef path As String, Optional ByVal suffix As String = "") As Boolean
    path = BuildExportPath(ext, suffix)
    BeginExport = ArchiveToOld(path)
End Function

Private Function ExportSaveAs(ByVal ext As String, ByRef path As String) As Integer
    If BeginExport(ext, path) Then ExportSaveAs = OkFail(SaveAsSilent(path)) Else ExportSaveAs = 1
End Function

Private Function StlAssemblyPass(ByVal path As String, ByVal oneFile As Boolean) As Integer
    Dim archived As Boolean
    SetPrefToggle swSTLComponentsIntoOneFile, oneFile
    If oneFile Then archived = ArchiveToOld(path) Else archived = ArchiveAssemblyComponentStls()
    If Not archived Then StlAssemblyPass = 1: Exit Function
    StlAssemblyPass = OkFail(SaveAsSilent(path))

    If StlAssemblyPass = 0 And Not oneFile And bRemoveSpaces Then NormalizeComponentStlNames
End Function

Private Function SaveAsSilent(ByVal path As String) As Boolean
    Dim errn As Long, warn As Long, exportData As Object, ok As Boolean, existed As Boolean
    existed = fso().FileExists(path)

    On Error Resume Next
    Err.Clear
    ok = swModel.Extension.SaveAs(path, 0, 3, exportData, errn, warn)
    If Not ok Then Err.Clear: ok = swModel.Extension.SaveAs(path, 0, 1, exportData, errn, warn)

    If Not ok And LCase$(fso().GetExtensionName(path)) = "stl" Then
        Err.Clear: errn = swModel.SaveAs3(path, 0, 1): ok = (errn = 0)
    End If

    SaveAsSilent = (Err.Number = 0 And ok)
    If Not SaveAsSilent Then Err.Clear: ReportExportFail path, existed
End Function

Private Function ExportSheetMetalDxf(ByVal path As String) As Integer
    Dim ok As Boolean, existed As Boolean
    existed = fso().FileExists(path)
    On Error Resume Next
    Err.Clear
    ok = swModel.ExportToDWG2(path, CStr(swModel.GetPathName), 1, True, Empty, False, False, 5, Empty)
    If Err.Number <> 0 Or Not ok Then
        Err.Clear: ReportExportFail path, existed: ExportSheetMetalDxf = 1
    Else
        ExportSheetMetalDxf = 0
    End If
End Function

Private Function BuildExportPath(ByVal ext As String, ByVal suffix As String) As String
    BuildExportPath = fso().BuildPath(mFolder, NormalizeFileName(mBase & suffix & "." & ext))
End Function

Private Function NormalizeFileName(ByVal fileName As String) As String
    NormalizeFileName = IIf(bRemoveSpaces, LCase$(Replace(fileName, " ", "_")), fileName)
End Function

Private Function IsComponentStlName(ByVal fileName As String) As Boolean
    Dim nm As String: nm = LCase$(fso().GetBaseName(fileName))
    IsComponentStlName = (nm Like LCase$(mBase) & " - *") Or (nm Like LCase$(Replace(mBase, " ", "_")) & "_-_*")
End Function

Private Function ComponentStlFiles() As Collection
    Dim f As Object, col As New Collection
    For Each f In fso().GetFolder(mFolder).Files
        If LCase$(fso().GetExtensionName(f.name)) = "stl" And IsComponentStlName(f.name) Then col.Add f
    Next f
    Set ComponentStlFiles = col
End Function

Private Function ArchiveAssemblyComponentStls() As Boolean
    Dim f As Object
    For Each f In ComponentStlFiles()
        If Not ArchiveToOld(f.path) Then ArchiveAssemblyComponentStls = False: Exit Function
    Next f
    ArchiveAssemblyComponentStls = True
End Function

Private Sub NormalizeComponentStlNames()
    Dim f As Object, newName As String, newPath As String
    If Not bRemoveSpaces Then Exit Sub
    For Each f In ComponentStlFiles()
        newName = NormalizeFileName(f.name)
        If StrComp(newName, f.name, vbBinaryCompare) <> 0 Then
            newPath = fso().BuildPath(mFolder, newName)
            On Error Resume Next
            If fso().FileExists(newPath) Then fso().DeleteFile newPath, True
            f.name = newName: Err.Clear: On Error GoTo 0
        End If
    Next f
End Sub

Private Sub ReleaseExcel(ByRef xl As Object, ByRef wb As Object)
    On Error Resume Next
    If Not wb Is Nothing Then wb.Close False: Set wb = Nothing
    If Not xl Is Nothing Then xl.Quit: Set xl = Nothing
End Sub

Private Sub ReportExportFail(ByVal path As String, ByVal existed As Boolean)
    If existed Then NotifyFileLocked path Else MsgErr "Export failed:" & vbCrLf & path
End Sub

Private Sub MsgErr(ByVal text As String)
    MsgBox text, vbExclamation, "ExportTools"
End Sub

Private Sub NotifyFileLocked(ByVal targetPath As String)
    MsgErr "Close the file (it may be open in another program):" & vbCrLf & targetPath
End Sub

'===== ARCHIVE ================================================================

Private Function ArchiveToOld(ByVal finalPath As String) As Boolean
    Dim oldDir As String, base As String, ext As String
    Dim stamp As String, dest As String, n As Long

    On Error GoTo Fail
    If Len(finalPath) = 0 Then ArchiveToOld = True: Exit Function
    With fso()
        finalPath = .GetAbsolutePathName(finalPath)

        If Not .FileExists(finalPath) Then ArchiveToOld = True: Exit Function

        oldDir = .BuildPath(.GetParentFolderName(finalPath), "old")
        If Not .FolderExists(oldDir) Then .CreateFolder oldDir

        base = .GetBaseName(finalPath)
        ext = .GetExtensionName(finalPath)
        stamp = Format$(.GetFile(finalPath).DateLastModified, "yyyymmdd.hhnnss")
        dest = .BuildPath(oldDir, base & "_" & stamp & "." & ext)

        n = 2
        Do While .FileExists(dest)
            dest = .BuildPath(oldDir, base & "_" & stamp & "_" & n & "." & ext)
            n = n + 1
        Loop

        On Error Resume Next
        Err.Clear: .MoveFile finalPath, dest
        If Err.Number = 0 And .FileExists(dest) And Not .FileExists(finalPath) Then ArchiveToOld = True: Exit Function

        Err.Clear: .CopyFile finalPath, dest, True
        If Err.Number = 0 And .FileExists(dest) Then ArchiveToOld = True: Exit Function
    End With

Fail:
    MsgErr "Failed to archive into old\:" & vbCrLf & finalPath
    ArchiveToOld = False
End Function
'==============================================================================
' ExportTools.bas — SolidWorks VBA export macro (single file)
'
' WHAT THIS MACRO DOES
'   Exports the active document next to the model file:
'     PDF, DXF (sheet-metal flat / drawing), STL, Excel (drawing tables).
'   Before overwriting an existing export, the old file is moved/copied into
'   a subfolder named "old\" with a timestamp from DateLastModified.
'
' HOW TO USE
'   1) Edit the SETTINGS constants below (True/False and mode numbers).
'   2) In SolidWorks: Tools > Macro > Run > select this .bas (or assign to a button).
'   3) The active document must already be saved on disk (needs a real path).
'
' RETURN CODES (each Export* function)
'   0 = success (OK)
'   1 = failure (FAIL)
'
' TYPICAL FLOW FOR ONE FORMAT
'   BuildExportPath > ArchiveToOld (if file exists) > write new file
'
' NOTES FOR BEGINNERS
'   - "Private" = only visible inside this module; "Public" = callable from outside.
'   - "Const" = constant value you change at the top; do not change SW API constants.
'   - Late binding: we use Object + CreateObject/GetObject (no SW type library required).
'   - On Error Resume Next = ignore errors for the next statements (used around SW/Excel calls).
'==============================================================================

Option Explicit

'----- SETTINGS (edit these) --------------------------------------------------
' Turn each export mode on/off. Only enabled modes run from main().

Private Const bExportPdf As Boolean = True
' True  = export PDF of the active document (any type that SaveAs supports).

Private Const bExportDxf As Boolean = True
' True  = export DXF.
' Allowed only for: sheet-metal Part (flat pattern) OR Drawing.

Private Const bExportStl As Boolean = True
' True  = export STL.
' Allowed only for: Part OR Assembly (not Drawing).

Private Const bExportExcel As Boolean = True
' True  = dump drawing tables into an .xlsx file.
' Allowed only for: Drawing that contains at least one table.

Private Const iStlAssemblyMode As Integer = 3
' How to export STL when the active document is an Assembly:
'   1 = one combined STL file (assembly as a single mesh)
'   2 = separate STL per component (SolidWorks naming: "Asm - Comp^Asm-1.STL")
'   3 = both (1 then 2)
' Ignored for Parts (always one file).

Private Const bDxfAddThickness As Boolean = True
' True  = for sheet-metal Part DXF, append thickness to the file name:
'         e.g. "Bracket (S 2 mm).dxf"
' Thickness is read from the SheetMetal feature (meters > mm).

Private Const bRemoveSpaces As Boolean = True
' True  = normalize EVERY export file name:
'         spaces > underscore "_", and entire name to lowercase.
'         Example: "My Part (S 2 mm).dxf" > "my_part_(s_2_mm).dxf"
' Also renames per-component STL files after SolidWorks creates them.

Private Const iNotify As Integer = 1
' 0 = show MsgBox only when something failed
' 1 = always show a summary MsgBox (OK/FAIL per mode)

Private Const swDocPART As Long = 1
Private Const swDocASSEMBLY As Long = 2
Private Const swDocDRAWING As Long = 3
Private Const swSTLComponentsIntoOneFile As Long = 72

Private Const swDxfOutputNoScale As Long = 136

Private swApp As Object
Private swModel As Object
Private mBase As String
Private mFolder As String

'===== ENTRY ==================================================================

Public Sub main()
    Dim Fail As Integer, msg As String

    On Error Resume Next
    Set swApp = GetObject(, "SldWorks.Application")
    On Error GoTo 0
    If swApp Is Nothing Then MsgErr "Cannot connect to SolidWorks.": Exit Sub

    Set swModel = swApp.ActiveDoc
    If swModel Is Nothing Then MsgErr "No active document.": Exit Sub

    If Len(swModel.GetPathName) = 0 Then MsgErr "Document must be saved before export.": Exit Sub

    With fso()
        mBase = .GetBaseName(swModel.GetPathName)
        mFolder = .GetParentFolderName(swModel.GetPathName)
    End With

    Fail = 0: msg = ""
    If bExportPdf Then Acc "PDF", ExportPdf(), Fail, msg
    If bExportDxf And CanExportDxf() Then Acc "DXF", ExportDxf(), Fail, msg
    If bExportStl And CanExportStl() Then Acc "STL", ExportStl(), Fail, msg
    If bExportExcel And CanExportExcel() Then Acc "Excel", ExportExcel(), Fail, msg

    If Len(msg) = 0 Then msg = "No export modes enabled.": Fail = 1
    If iNotify = 1 Or Fail <> 0 Then MsgBox Trim$(msg), IIf(Fail = 0, vbInformation, vbExclamation), "ExportTools"
End Sub

Private Sub Acc(ByVal label As String, ByVal code As Integer, ByRef Fail As Integer, ByRef msg As String)
    Fail = Fail Or code
    msg = msg & label & ": " & IIf(code = 0, "OK", "FAIL") & vbCrLf
End Sub

'===== EXPORT MODES ===========================================================

Public Function ExportPdf() As Integer
    Dim path As String: ExportPdf = ExportSaveAs("pdf", path)
End Function

Public Function ExportDxf() As Integer
    Dim path As String, prevScale As Long, suffix As String, thk As Double
    If Not CanExportDxf() Then ExportDxf = 1: Exit Function

    If bDxfAddThickness And DocType() = swDocPART Then
        thk = GetSheetMetalThicknessMm()
        If thk > 0 Then suffix = " (S " & Format$(thk, "0.##") & " mm)"
    End If

    If Not BeginExport("dxf", path, suffix) Then ExportDxf = 1: Exit Function

    prevScale = PrefInt(swDxfOutputNoScale): SetPrefInt swDxfOutputNoScale, 1
    If DocType() = swDocPART Then
        ExportDxf = ExportSheetMetalDxf(path)
    Else
        ExportDxf = OkFail(SaveAsSilent(path))
    End If
    SetPrefInt swDxfOutputNoScale, prevScale
End Function

Public Function ExportStl() As Integer
    Dim path As String, prevOneFile As Boolean, Fail As Integer
    If Not CanExportStl() Then ExportStl = 1: Exit Function

    If DocType() = swDocPART Then ExportStl = ExportSaveAs("stl", path): Exit Function

    path = BuildExportPath("stl", "")
    prevOneFile = PrefToggle(swSTLComponentsIntoOneFile)
    Select Case iStlAssemblyMode
        Case 1: Fail = StlAssemblyPass(path, True)
        Case 2: Fail = StlAssemblyPass(path, False)
        Case 3: Fail = StlAssemblyPass(path, True) Or StlAssemblyPass(path, False)
        Case Else: Fail = 1
    End Select
    SetPrefToggle swSTLComponentsIntoOneFile, prevOneFile
    ExportStl = Fail
End Function

Public Function ExportExcel() As Integer
    Dim path As String, xl As Object, wb As Object, ws As Object
    Dim v As Object, tbls As Variant, ti As Long, tbl As Object
    Dim rr As Long, row As Long, c As Long, code As Integer
    On Error GoTo Fail
    If Not CanExportExcel() Then ExportExcel = 1: Exit Function
    If Not BeginExport("xlsx", path) Then ExportExcel = 1: Exit Function

    Set xl = CreateObject("Excel.Application"): xl.DisplayAlerts = False
    Set wb = xl.Workbooks.Add: Set ws = wb.Worksheets(1): ws.name = "Tables": row = 1

    Set v = swModel.GetFirstView
    Do While Not v Is Nothing
        If v.GetTableAnnotationCount > 0 Then
            tbls = v.GetTableAnnotations
            If IsArray(tbls) Then
                For ti = LBound(tbls) To UBound(tbls)
                    Set tbl = tbls(ti)
                    If Not tbl Is Nothing Then

                        For rr = 0 To tbl.RowCount - 1
                            For c = 0 To tbl.ColumnCount - 1
                                ws.Cells(row, c + 1).value = TableCellText(tbl, rr, c)
                            Next c
                            row = row + 1
                        Next rr
                        row = row + 1
                    End If
                Next ti
            End If
        End If
        Set v = v.GetNextView
    Loop

    If row > 1 Then wb.SaveAs path, 51: code = 0 Else code = 1
    ReleaseExcel xl, wb
    ExportExcel = code
    Exit Function
Fail:

    ReleaseExcel xl, wb
    NotifyFileLocked path
    ExportExcel = 1
End Function

'===== RULES (when is each mode allowed?) =====================================

Private Function DocType() As Long
    DocType = swModel.GetType
End Function

Private Function CanExportDxf() As Boolean
    Dim t As Long: t = DocType()
    CanExportDxf = (t = swDocDRAWING) Or (t = swDocPART And IsSheetMetalPart())
End Function

Private Function CanExportStl() As Boolean
    Dim t As Long: t = DocType()
    CanExportStl = (t = swDocPART Or t = swDocASSEMBLY)
End Function

Private Function CanExportExcel() As Boolean
    CanExportExcel = (DocType() = swDocDRAWING) And DrawingHasTables()
End Function

Private Function DrawingHasTables() As Boolean
    Dim v As Object
    On Error Resume Next
    Set v = swModel.GetFirstView
    Do While Not v Is Nothing
        If v.GetTableAnnotationCount > 0 Then DrawingHasTables = True: Exit Function
        Set v = v.GetNextView
    Loop
End Function

Private Function IsSheetMetalPart() As Boolean
    IsSheetMetalPart = Not FindFeatureInTypes("SheetMetal", "FlatPattern", "SheetMetalFolder") Is Nothing
End Function

Private Function FindFeatureInTypes(ParamArray typeNames() As Variant) As Object
    Dim feat As Object, i As Long, tn As String
    On Error Resume Next
    Set feat = swModel.FirstFeature
    Do While Not feat Is Nothing
        tn = feat.GetTypeName2
        For i = LBound(typeNames) To UBound(typeNames)
            If StrComp(tn, CStr(typeNames(i)), vbTextCompare) = 0 Then
                Set FindFeatureInTypes = feat: Exit Function
            End If
        Next i
        Set feat = feat.GetNextFeature
    Loop
End Function

Private Function GetSheetMetalThicknessMm() As Double
    Dim feat As Object
    On Error Resume Next
    Set feat = FindFeatureInTypes("SheetMetal")
    If Not feat Is Nothing Then GetSheetMetalThicknessMm = feat.GetDefinition.thickness * 1000#
End Function

Private Function TableCellText(ByVal tbl As Object, ByVal r As Long, ByVal c As Long) As String
    Dim s As String
    On Error Resume Next
    Err.Clear: s = tbl.GetEntryText(r, c)
    If Err.Number <> 0 Or Len(s) = 0 Then Err.Clear: s = tbl.text(r, c)
    If Err.Number <> 0 Then s = ""
    TableCellText = StripHtml(s)
End Function

Private Function StripHtml(ByVal s As String) As String
    Dim i As Long, ch As String, inTag As Boolean, out As String
    For i = 1 To Len(s)
        ch = Mid$(s, i, 1)
        If ch = "<" Then
            inTag = True
        ElseIf ch = ">" Then
            inTag = False
        ElseIf Not inTag Then
            out = out & ch
        End If
    Next i
    StripHtml = Trim$(out)
End Function

'===== PATH / WRITE HELPERS ===================================================

Private Function fso() As Object
    Static o As Object
    If o Is Nothing Then Set o = CreateObject("Scripting.FileSystemObject")
    Set fso = o
End Function

Private Function PrefInt(ByVal id As Long) As Long
    On Error Resume Next: PrefInt = swApp.GetUserPreferenceIntegerValue(id)
End Function

Private Sub SetPrefInt(ByVal id As Long, ByVal value As Long)
    On Error Resume Next: swApp.SetUserPreferenceIntegerValue id, value
End Sub

Private Function PrefToggle(ByVal id As Long) As Boolean
    On Error Resume Next: PrefToggle = swApp.GetUserPreferenceToggle(id)
End Function

Private Sub SetPrefToggle(ByVal id As Long, ByVal value As Boolean)
    On Error Resume Next: swApp.SetUserPreferenceToggle id, value
End Sub

Private Function OkFail(ByVal ok As Boolean) As Integer
    OkFail = IIf(ok, 0, 1)
End Function

Private Function BeginExport(ByVal ext As String, ByRef path As String, Optional ByVal suffix As String = "") As Boolean
    path = BuildExportPath(ext, suffix)
    BeginExport = ArchiveToOld(path)
End Function

Private Function ExportSaveAs(ByVal ext As String, ByRef path As String) As Integer
    If BeginExport(ext, path) Then ExportSaveAs = OkFail(SaveAsSilent(path)) Else ExportSaveAs = 1
End Function

Private Function StlAssemblyPass(ByVal path As String, ByVal oneFile As Boolean) As Integer
    Dim archived As Boolean
    SetPrefToggle swSTLComponentsIntoOneFile, oneFile
    If oneFile Then archived = ArchiveToOld(path) Else archived = ArchiveAssemblyComponentStls()
    If Not archived Then StlAssemblyPass = 1: Exit Function
    StlAssemblyPass = OkFail(SaveAsSilent(path))

    If StlAssemblyPass = 0 And Not oneFile And bRemoveSpaces Then NormalizeComponentStlNames
End Function

Private Function SaveAsSilent(ByVal path As String) As Boolean
    Dim errn As Long, warn As Long, exportData As Object, ok As Boolean, existed As Boolean
    existed = fso().FileExists(path)

    On Error Resume Next
    Err.Clear
    ok = swModel.Extension.SaveAs(path, 0, 3, exportData, errn, warn)
    If Not ok Then Err.Clear: ok = swModel.Extension.SaveAs(path, 0, 1, exportData, errn, warn)

    If Not ok And LCase$(fso().GetExtensionName(path)) = "stl" Then
        Err.Clear: errn = swModel.SaveAs3(path, 0, 1): ok = (errn = 0)
    End If

    SaveAsSilent = (Err.Number = 0 And ok)
    If Not SaveAsSilent Then Err.Clear: ReportExportFail path, existed
End Function

Private Function ExportSheetMetalDxf(ByVal path As String) As Integer
    Dim ok As Boolean, existed As Boolean
    existed = fso().FileExists(path)
    On Error Resume Next
    Err.Clear
    ok = swModel.ExportToDWG2(path, CStr(swModel.GetPathName), 1, True, Empty, False, False, 5, Empty)
    If Err.Number <> 0 Or Not ok Then
        Err.Clear: ReportExportFail path, existed: ExportSheetMetalDxf = 1
    Else
        ExportSheetMetalDxf = 0
    End If
End Function

Private Function BuildExportPath(ByVal ext As String, ByVal suffix As String) As String
    BuildExportPath = fso().BuildPath(mFolder, NormalizeFileName(mBase & suffix & "." & ext))
End Function

Private Function NormalizeFileName(ByVal fileName As String) As String
    NormalizeFileName = IIf(bRemoveSpaces, LCase$(Replace(fileName, " ", "_")), fileName)
End Function

Private Function IsComponentStlName(ByVal fileName As String) As Boolean
    Dim nm As String: nm = LCase$(fso().GetBaseName(fileName))
    IsComponentStlName = (nm Like LCase$(mBase) & " - *") Or (nm Like LCase$(Replace(mBase, " ", "_")) & "_-_*")
End Function

Private Function ComponentStlFiles() As Collection
    Dim f As Object, col As New Collection
    For Each f In fso().GetFolder(mFolder).Files
        If LCase$(fso().GetExtensionName(f.name)) = "stl" And IsComponentStlName(f.name) Then col.Add f
    Next f
    Set ComponentStlFiles = col
End Function

Private Function ArchiveAssemblyComponentStls() As Boolean
    Dim f As Object
    For Each f In ComponentStlFiles()
        If Not ArchiveToOld(f.path) Then ArchiveAssemblyComponentStls = False: Exit Function
    Next f
    ArchiveAssemblyComponentStls = True
End Function

Private Sub NormalizeComponentStlNames()
    Dim f As Object, newName As String, newPath As String
    If Not bRemoveSpaces Then Exit Sub
    For Each f In ComponentStlFiles()
        newName = NormalizeFileName(f.name)
        If StrComp(newName, f.name, vbBinaryCompare) <> 0 Then
            newPath = fso().BuildPath(mFolder, newName)
            On Error Resume Next
            If fso().FileExists(newPath) Then fso().DeleteFile newPath, True
            f.name = newName: Err.Clear: On Error GoTo 0
        End If
    Next f
End Sub

Private Sub ReleaseExcel(ByRef xl As Object, ByRef wb As Object)
    On Error Resume Next
    If Not wb Is Nothing Then wb.Close False: Set wb = Nothing
    If Not xl Is Nothing Then xl.Quit: Set xl = Nothing
End Sub

Private Sub ReportExportFail(ByVal path As String, ByVal existed As Boolean)
    If existed Then NotifyFileLocked path Else MsgErr "Export failed:" & vbCrLf & path
End Sub

Private Sub MsgErr(ByVal text As String)
    MsgBox text, vbExclamation, "ExportTools"
End Sub

Private Sub NotifyFileLocked(ByVal targetPath As String)
    MsgErr "Close the file (it may be open in another program):" & vbCrLf & targetPath
End Sub

'===== ARCHIVE ================================================================

Private Function ArchiveToOld(ByVal finalPath As String) As Boolean
    Dim oldDir As String, base As String, ext As String
    Dim stamp As String, dest As String, n As Long

    On Error GoTo Fail
    If Len(finalPath) = 0 Then ArchiveToOld = True: Exit Function
    With fso()
        finalPath = .GetAbsolutePathName(finalPath)

        If Not .FileExists(finalPath) Then ArchiveToOld = True: Exit Function

        oldDir = .BuildPath(.GetParentFolderName(finalPath), "old")
        If Not .FolderExists(oldDir) Then .CreateFolder oldDir

        base = .GetBaseName(finalPath)
        ext = .GetExtensionName(finalPath)
        stamp = Format$(.GetFile(finalPath).DateLastModified, "yyyymmdd.hhnnss")
        dest = .BuildPath(oldDir, base & "_" & stamp & "." & ext)

        n = 2
        Do While .FileExists(dest)
            dest = .BuildPath(oldDir, base & "_" & stamp & "_" & n & "." & ext)
            n = n + 1
        Loop

        On Error Resume Next
        Err.Clear: .MoveFile finalPath, dest
        If Err.Number = 0 And .FileExists(dest) And Not .FileExists(finalPath) Then ArchiveToOld = True: Exit Function

        Err.Clear: .CopyFile finalPath, dest, True
        If Err.Number = 0 And .FileExists(dest) Then ArchiveToOld = True: Exit Function
    End With

Fail:
    MsgErr "Failed to archive into old\:" & vbCrLf & finalPath
    ArchiveToOld = False
End Function