BOM balloons without a parts list

Hi,

I am writing a journal to allow users to create a BOM balloon that is automatically populated with the correct find number. I know this functionality exists with parts lists, but we use TeamCenter and do not want parts lists on our drawings (even placing them off the page has been ruled out!).

I can manually create a balloon that is linked to the find number of the component by adding an identification symbol, editing the text and changing the category to 'Relationships' and clicking the 'Insert Object Attribute' button and then selecting my component. This produces text such as <W!47198@CALLOUT>.

My question is: Where does the number 47198 come from in that text? And can I find out that number from within my journal?

Cheers,

Ian

I'd guess that is the ID of the component. Run the following code on a component to see if the ID numbers match (code written by Amy Webster of GTAC).

Option Strict Off
Imports System
Imports NXOpen
Imports NXOpen.UF

Module NXJournal

Dim s As Session = Nothing
Dim ufs As UFSession = Nothing
Dim lw As ListingWindow = Nothing

Sub Main
s = Session.GetSession()
lw = s.ListingWindow
ufs = NXOpen.UF.UFSession.GetUFSession()

Dim object1 As NXObject = SelectAnObject("Report Object ID")
Dim handle As String = Nothing
Dim fileData As String = Nothing
Dim ID as UInteger = 0
Dim version as UInteger = 0

Do While object1 isnot nothing
handle = ufs.Tag.AskHandleOfTag(object1.Tag)
ufs.Tag.DecomposeHandle(handle, fileData, ID, version)
ECHO("ID = " & ID)

object1 = SelectAnObject("Report Object ID")
Loop
End Sub

Function SelectAnObject(ByVal prompt As String) As NXObject

Dim selobj As NXObject = Nothing
Dim theUI As UI = UI.GetUI
Dim cursor As Point3d
Dim typeArray() As Selection.SelectionType = _
{Selection.SelectionType.All, _
Selection.SelectionType.Faces, _
Selection.SelectionType.Edges}

Dim resp As Selection.Response = theUI.SelectionManager.SelectObject( _
prompt, "Select an object", _
Selection.SelectionScope.AnyInAssembly, _
False, typeArray, selobj, cursor)

Return selobj
End Function

Sub Echo(ByVal output As String)
lw.Open()
lw.WriteLine(output)
ufs.UF.PrintSyslog(output & vbCrLf, False)
End Sub

End Module

Oh, and be aware that this all changes in NX 8 or higher.
http://community.plm.automation.siemens.com/t5/NX-Languages/Reading-Text...

Hi,

Thanks for hint about the handles. It wasn't what was needed, but it pointed me in the right direction. The number is actually the component Tag. I've used it in the code below, which allows the user to select an edge, then finds the component or subassembly and correctly populates a callout balloon.

The bit that I'm stuck on is attaching the leader to the geometry on the view. The arrow always ends up in the bottom left of the drawing, not attached to anything. If you have any advice on this front, it would be greatly appreciated.

Cheers,
Ian

' NX 7.5.5.4
' Journal created by Ian Eldred on Mon Feb 16 12:55:59 2015 GMT Standard Time
' Last updated by Ian Eldred on 01/05/2015.

Option Strict Off
Imports System
Imports NXOpen
Imports NXOpen.UF
Imports NXOpenUI

Module AutoBalloon
Dim theSession As Session = Session.GetSession()
Dim workPart As Part = theSession.Parts.Work
Dim displayPart As Part = theSession.Parts.Display
Dim lw As ListingWindow = theSession.ListingWindow
Dim ufs As UFSession = UFSession.GetUFSession
Dim theUI As UI = UI.GetUI

Sub Main()

Dim myEdge As Object
Dim myPoint As Point3d
Dim myPointBalloon As Point3d
Dim type As Integer
Dim subtype As Integer
Dim handle As String = Nothing
Dim fileData As String = Nothing
Dim ID As UInteger = 0
Dim version As UInteger = 0
Dim strCallout As String
Dim myComponent As Assemblies.Component

' ----------------------------------------------
' Menu: Insert->Annotation->Identification Symbol...
' ----------------------------------------------
Dim markId1 As Session.UndoMarkId
markId1 = theSession.SetUndoMark(Session.MarkVisibility.Visible, "Start")

'Select edge
If UserSelectEdge("Select edge to attach balloon", myEdge, myPoint) = Selection.Response.Cancel Then
Return
Else
'Find out if the selected component is a child of the current assembly or if it is a child of a subassembly.
myComponent = myEdge.OwningComponent
Do Until workPart.ComponentAssembly.RootComponent.DisplayName = myComponent.Parent.Parent.DisplayName
myComponent = myComponent.Parent
Loop
strCallout = ""
End If

Dim nullAnnotations_IdSymbol As Annotations.IdSymbol = Nothing

Dim idSymbolBuilder1 As Annotations.IdSymbolBuilder
idSymbolBuilder1 = workPart.Annotations.IdSymbols.CreateIdSymbolBuilder(nullAnnotations_IdSymbol)
idSymbolBuilder1.UpperText = strCallout
idSymbolBuilder1.Size = 12.0

Dim leaderData1 As Annotations.LeaderData
leaderData1 = workPart.Annotations.CreateLeaderData()
leaderData1.Arrowhead = Annotations.LeaderData.ArrowheadType.FilledArrow
leaderData1.TerminatorType = Annotations.LeaderData.LeaderType.PlainWithoutStub

idSymbolBuilder1.Leader.Leaders.Append(leaderData1)
idSymbolBuilder1.Origin.SetInferRelativeToGeometry(True)

Dim nullView As View = Nothing

'Arrow location
'Why 2 identical points?
Dim coordinates1 As Point3d = New Point3d(myPoint.X, myPoint.Y, 0.0)
Dim point1 As Point
point1 = workPart.Points.CreatePoint(coordinates1)
Dim point2 As Point3d = New Point3d(myPoint.X, myPoint.Y, 0.0)
leaderData1.Leader.SetValue(point1, workPart.Views.WorkView, point2)

Dim assocOrigin1 As Annotations.Annotation.AssociativeOriginData
idSymbolBuilder1.Origin.SetAssociativeOrigin(assocOrigin1)

'Balloon location
Dim response2 As Selection.DialogResponse = UserSelectScreenPos("Place balloon", myPointBalloon)
If response2 <> Selection.DialogResponse.Pick Then
Return
End If

Dim point3 As Point3d = New Point3d(myPointBalloon.X, myPointBalloon.Y, 0.0)
idSymbolBuilder1.Origin.Origin.SetValue(Nothing, nullView, point3)

Dim nXObject1 As NXObject
nXObject1 = idSymbolBuilder1.Commit()

idSymbolBuilder1.Destroy()
End Sub

Function UserSelectEdge(ByVal prompt As String, ByRef selObj As TaggedObject, ByRef selPoint As Point3d) As Selection.Response

'Allow user to interactively select an edge

Dim title As String = "Select an edge"
Dim includeFeatures As Boolean = False
Dim keepHighlighted As Boolean = False
Dim selAction As Selection.SelectionAction = Selection.SelectionAction.ClearAndEnableSpecific
Dim scope As Selection.SelectionScope = Selection.SelectionScope.AnyInAssembly
Dim selectionMask_array(6) As Selection.MaskTriple

'Set the selection criteria to any edge
'TODO: Add point on surface
selectionMask_array(0).Type = UFConstants.UF_solid_type
selectionMask_array(0).Subtype = UFConstants.UF_UI_SEL_FEATURE_ANY_EDGE
selectionMask_array(0).SolidBodySubtype = UFConstants.UF_UI_SEL_FEATURE_ANY_EDGE

selectionMask_array(1).Type = UFConstants.UF_line_type
selectionMask_array(1).Subtype = UFConstants.UF_all_subtype

selectionMask_array(2).Type = UFConstants.UF_circle_type
selectionMask_array(2).Subtype = UFConstants.UF_all_subtype

selectionMask_array(3).Type = UFConstants.UF_conic_type
selectionMask_array(3).Subtype = UFConstants.UF_all_subtype

selectionMask_array(4).Type = UFConstants.UF_spline_type
selectionMask_array(4).Subtype = UFConstants.UF_all_subtype

selectionMask_array(5).Type = UFConstants.UF_solid_silhouette_type
selectionMask_array(5).Subtype = UFConstants.UF_all_subtype

selectionMask_array(6).Type = UFConstants.UF_section_edge_type
selectionMask_array(6).Subtype = UFConstants.UF_all_subtype

'This line allows the user to select from any view:
ufs.Ui.SetCursorView(0)

Dim resp As Selection.Response = theUI.SelectionManager.SelectTaggedObject(prompt, _
title, scope, selAction, _
includeFeatures, keepHighlighted, selectionMask_array, _
selObj, selPoint)
If resp = Selection.Response.ObjectSelected OrElse resp = Selection.Response.ObjectSelectedByName Then
Return Selection.Response.Ok
Else
Return Selection.Response.Cancel
End If

End Function

Function UserSelectScreenPos(ByVal prompt As String, ByRef selPoint As Point3d) As Selection.DialogResponse
'Allow user to interactively select a screen position
Dim view As NXOpen.View = Nothing
Return theUI.SelectionManager.SelectScreenPosition(prompt, view, selPoint)
End Function

End Module

The picked point is reported with respect to 3D model space; the leader point is being specified in terms of the 2D drawing sheet space. You will need to convert from the drawing view coordinates to the drawing sheet coordinates.

You might try the .MapModelToDrawing method.

Also, you might want to look at:
http://nxjournaling.com/content/move-parts-list-callout-leader-location
This journal allows the user to select an edge in the drawing and attach the callout balloon to the selected edge.

Thank you for that. The .MapModelToDrawing method looks like it could work for me. This method requires me to provide a view, so how do I identify the view that the user has clicked on (without asking them to select a view with a separate click)?

In the code linked to above, the .SelectWithSingleDialog method was used, which will return the point and the view selected.

I've just reviewed your parts list leader link above and I think that answers my question using the .SelectWithSingleDialog method. I'll give that a try with the .MapModelToDrawing method and see if I can get it to work. Either way, I'll post back here and let you know. Thanks again!

I now have this working pretty much as intended. See code below, much of the which was taken from: http://nxjournaling.com/content/move-parts-list-callout-leader-location.
As ever, thank you very much for your help!

' NX 7.5.5.4
' Journal created by Ian Eldred on 14/04/2015
' This journal allows a user to select an edge of a component on an assembly drawing, then
' select a location to place the balloon. The callout within the balloon will be associated
' to the callout of the component.
'################################################################################################################
' Version History:
' Version | Date | Changed by | Description of change
' --------|------------|---------------|-------------------------------------------------------------------------
' 1.0 | 29/04/2015 | Ian Eldred | Initial release
'################################################################################################################

Option Strict Off
Imports System
Imports System.Windows.Forms
Imports System.Collections.Generic
Imports NXOpen
Imports NXOpen.UF
Imports NXOpenUI

Module AutoBalloon
Dim theSession As Session = Session.GetSession()
Dim workPart As Part = theSession.Parts.Work
Dim displayPart As Part = theSession.Parts.Display
Dim lw As ListingWindow = theSession.ListingWindow
Dim lg As LogFile = theSession.LogFile
Dim ufs As UFSession = UFSession.GetUFSession
Dim theUI As UI = UI.GetUI
Dim dicSectionEdges As New Dictionary(Of Tag, Tag)
Dim viewList As New List(Of Tag)

Sub Main()

Dim myPointBalloon As Point3d
Dim strCallout As String
Dim myComponent As Assemblies.Component
Dim mySelPoint(2) As Double
Dim mySelViewTag As Tag = Tag.Null
Dim mySelEdgeTag As Tag = Nothing
Dim myEdgeCurve As DisplayableObject

lw.Open()
lg.WriteLine("~~ Auto balloon without parts list ~~")
lg.WriteLine(" timestamp: " & Now)

If IsNothing(theSession.Parts.Work) Then
'active part required
lg.WriteLine(" no active part, exiting journal")
Return
End If

Const undoMarkName As String = "Auto balloon without parts list"
Dim markId1 As Session.UndoMarkId
markId1 = theSession.SetUndoMark(Session.MarkVisibility.Visible, undoMarkName)

Do Until UserSelectEdge("Select edge to attach balloon", mySelPoint, mySelViewTag, mySelEdgeTag) = Selection.Response.Cancel

lg.WriteLine(" User has selected an edge.")
'return the object from the given tag
ufs.Disp.SetHighlight(mySelEdgeTag, 0)
myEdgeCurve = Utilities.NXObjectManager.Get(mySelEdgeTag)
If IsNothing(myEdgeCurve) Then
lg.WriteLine(" UserSelectEdge() returned Nothing, skip to next edge selection")
Continue Do
End If

lg.WriteLine(" selected object type: " & myEdgeCurve.GetType.ToString)
Dim selView As NXOpen.View = Utilities.NXObjectManager.Get(mySelViewTag)
lg.WriteLine(" selected in view: " & selView.Name)
lg.WriteLine(" View type: " & selView.GetType.ToString)
lg.WriteLine(" selected point: " & mySelPoint(0) & ", " & mySelPoint(1) & ", " & mySelPoint(2))

If TypeOf (selView) Is Drawings.SectionView Then
lg.WriteLine(" View is a section view. Building dictionary of sections edges.")
BuildSectionDictionary(selView)
End If

lg.WriteLine(" Get the component from the selected edge.")
myComponent = GetComponentOfEdge(mySelEdgeTag)
lg.WriteLine(" mySelEdgeTag = " & mySelEdgeTag.ToString)
If IsNothing(myComponent) Then
lg.WriteLine(" Could not find component of edge, skip to next edge selection")
Continue Do
End If
lg.WriteLine(" GetComponentOfEdge(mySelEdgeTag) = " & myComponent.DisplayName)

lg.WriteLine(" Find out if the selected component is a child of the current assembly or if it is a child of a subassembly.")
Do Until workPart.ComponentAssembly.RootComponent.DisplayName = myComponent.Parent.Parent.DisplayName
lg.WriteLine(" myComponent = " & myComponent.DisplayName & ": No")
myComponent = myComponent.Parent
Loop
strCallout = ""
lg.WriteLine(" myComponent = " & myComponent.DisplayName & ": Yes")
lg.WriteLine(" strCallout = " & strCallout)

Dim symPt As Point3d
symPt.X = mySelPoint(0)
symPt.Y = mySelPoint(1)
symPt.Z = mySelPoint(2)

lg.WriteLine(" Building the new ID symbol...")
Dim nullAnnotations_IdSymbol As Annotations.IdSymbol = Nothing

Dim idSymbolBuilder1 As Annotations.IdSymbolBuilder
idSymbolBuilder1 = workPart.Annotations.IdSymbols.CreateIdSymbolBuilder(nullAnnotations_IdSymbol)
idSymbolBuilder1.UpperText = strCallout
idSymbolBuilder1.Size = 12.0

Dim leaderData1 As Annotations.LeaderData
leaderData1 = workPart.Annotations.CreateLeaderData()
leaderData1.Arrowhead = Annotations.LeaderData.ArrowheadType.FilledArrow
leaderData1.TerminatorType = Annotations.LeaderData.LeaderType.PlainWithoutStub

idSymbolBuilder1.Leader.Leaders.Append(leaderData1)
idSymbolBuilder1.Origin.SetInferRelativeToGeometry(True)

Dim nullView As NXOpen.View = Nothing

'Arrow location
lg.WriteLine(" Creating the leader.")
leaderData1.Leader.SetValue(myEdgeCurve, selView, symPt)

Dim assocOrigin1 As Annotations.Annotation.AssociativeOriginData
idSymbolBuilder1.Origin.SetAssociativeOrigin(assocOrigin1)

'Balloon location
lg.WriteLine(" Getting the screen position to place the balloon.")
Dim response2 As Selection.DialogResponse = UserSelectScreenPos("Place balloon", myPointBalloon)
If response2 <> Selection.DialogResponse.Pick Then
Return
End If

Dim point3 As Point3d = New Point3d(myPointBalloon.X, myPointBalloon.Y, 0.0)
idSymbolBuilder1.Origin.Origin.SetValue(Nothing, nullView, point3)

lg.WriteLine(" Committing the ID symbol.")
Dim nXObject1 As NXObject
nXObject1 = idSymbolBuilder1.Commit()

idSymbolBuilder1.Destroy()

Loop
lg.WriteLine("~~ End Auto balloon without parts list ~~")
lw.Close()

End Sub

Function UserSelectEdge(ByVal prompt As String, ByRef theCursor() As Double, ByRef theView As Tag, ByRef theObject As Tag) As Selection.Response

'Allow user to interactively select an edge
Dim response As Integer = 0
Dim user_data As System.IntPtr

ufs.Ui.LockUgAccess(UFConstants.UF_UI_FROM_CUSTOM)
Dim curCursorView As Integer
ufs.Ui.AskCursorView(curCursorView)

Try
ufs.Ui.SetCursorView(0)
'SelectWithSingleDialog allows the user to make a single selection and returns the object, the point and the view.
ufs.Ui.SelectWithSingleDialog("Select component: ", prompt, _
UFConstants.UF_UI_SEL_SCOPE_ANY_IN_ASSEMBLY, AddressOf init_proc_body, _
user_data, response, theObject, theCursor, theView)
Finally
ufs.Ui.SetCursorView(curCursorView)
ufs.Ui.UnlockUgAccess(UFConstants.UF_UI_FROM_CUSTOM)
End Try

Select Case response
Case UFConstants.UF_UI_BACK
Return Selection.Response.Back
Case UFConstants.UF_UI_OK
Return Selection.Response.Ok
Case UFConstants.UF_UI_OBJECT_SELECTED
Return Selection.Response.ObjectSelected
Case UFConstants.UF_UI_OBJECT_SELECTED_BY_NAME
Return Selection.Response.ObjectSelectedByName
Case Else
Return Selection.Response.Cancel
End Select
End Function

Public Function init_proc_body(ByVal select_ As IntPtr, _
ByVal userdata As IntPtr) As Integer
'this function must have the same signature as UFUi.SelInitFnT Delegate

'setup mask to filter for edges or curves
Dim num_triples As Integer = 7
Dim mask_triples(num_triples - 1) As UFUi.Mask

mask_triples(0).object_type = UFConstants.UF_solid_type
mask_triples(0).object_subtype = UFConstants.UF_UI_SEL_FEATURE_ANY_EDGE
mask_triples(0).solid_type = UFConstants.UF_UI_SEL_FEATURE_ANY_EDGE

mask_triples(1).object_type = UFConstants.UF_line_type
mask_triples(1).object_subtype = UFConstants.UF_all_subtype

mask_triples(2).object_type = UFConstants.UF_circle_type
mask_triples(2).object_subtype = UFConstants.UF_all_subtype

mask_triples(3).object_type = UFConstants.UF_conic_type
mask_triples(3).object_subtype = UFConstants.UF_all_subtype

mask_triples(4).object_type = UFConstants.UF_spline_type
mask_triples(4).object_subtype = UFConstants.UF_all_subtype

mask_triples(5).object_type = UFConstants.UF_solid_silhouette_type
mask_triples(5).object_subtype = UFConstants.UF_all_subtype

mask_triples(6).object_type = UFConstants.UF_section_edge_type
mask_triples(6).object_subtype = UFConstants.UF_all_subtype

ufs.Ui.SetSelMask(select_, _
UFUi.SelMaskAction.SelMaskClearAndEnableSpecific, _
num_triples, mask_triples)

Return UFConstants.UF_UI_SEL_SUCCESS

End Function

Function GetComponentOfEdge(ByVal theEdgeTag As Tag) As Assemblies.Component
'Find the owning componet of the edge

Dim dwgObj As NXObject
dwgObj = Utilities.NXObjectManager.Get(theEdgeTag)

If dwgObj.IsOccurrence Then
'object is owned by a component (edge, face, etc)

Return dwgObj.OwningComponent
Else
'object is owned by the drawing (silhouette edge curve, section edge curve, extracted edge curve, etc)

If TypeOf (dwgObj) Is NXOpen.Line Or TypeOf (dwgObj) Is NXOpen.Arc Or _
TypeOf (dwgObj) Is NXOpen.Spline Or TypeOf (dwgObj) Is NXOpen.Conic Then

Dim groupTag As Tag
Dim groupType As Integer
Dim groupSubType As Integer
'determine if this is a section or silhouette curve
ufs.Draw.AskGroupOfCurve(dwgObj.Tag, groupTag, groupType, groupSubType)

Select Case groupType
Case Is = UFConstants.UF_solid_silhouette_type
'silhouette type 201
Dim theFaceTag As Tag
ufs.Draw.AskFaceOfSil(dwgObj.Tag, theFaceTag)
lg.WriteLine(" theFaceTag = " & theFaceTag)

Dim theFace As NXObject = Utilities.NXObjectManager.Get(theFaceTag)
lg.WriteLine(" theFace = " & theFace.ToString)
Return theFace.OwningComponent

Case Is = UFConstants.UF_solid_section_type
'section curve type 198

Dim tempComp As Assemblies.Component
tempComp = ComponentOfSectionEdge(dwgObj.Tag)

Return tempComp

Case Else
Return Nothing

End Select

End If
'theObject is not a line, arc, conic, or spline
Return Nothing
End If

End Function

Sub BuildSectionDictionary(ByVal sectionView As Drawings.SectionView)

'If you have previously built a dictionary for this view, exit sub.
If viewList.IndexOf(sectionView.Tag) = -1 Then
viewList.Add(sectionView.Tag)
Else
Exit Sub
End If

Dim sxSolidTags() As Tag
Dim numSxSolids As Integer
ufs.Draw.AskSxsolidsOfSxview(sectionView.Tag, Nothing, numSxSolids, sxSolidTags)

For Each sxSolidTag As Tag In sxSolidTags

Dim numSxEdges As Integer
Dim sxEdgeTags() As Tag
ufs.Draw.AskSxedgesOfSxsolid(sxSolidTag, numSxEdges, sxEdgeTags)

For Each sxEdgeTag As Tag In sxEdgeTags

dicSectionEdges.Add(sxEdgeTag, sxSolidTag)

Next

Next

End Sub

Function ComponentOfSectionEdge(ByVal sectionEdgeTag As Tag) As Assemblies.Component

Dim tempComp As Assemblies.Component

If dicSectionEdges.ContainsKey(sectionEdgeTag) Then
Dim sxSolidTag As Tag = dicSectionEdges.Item(sectionEdgeTag)

Dim solidTag As Tag
ufs.Draw.AskSolidOfSection(sxSolidTag, solidTag)

Dim tempSolid As Body = Utilities.NXObjectManager.Get(solidTag)
tempComp = tempSolid.OwningComponent

Return tempComp

Else
Return Nothing
End If

End Function

Function UserSelectScreenPos(ByVal prompt As String, ByRef selPoint As Point3d) As Selection.DialogResponse
'Allow user to interactively select a screen position
Dim view As NXOpen.View = Nothing
Return theUI.SelectionManager.SelectScreenPosition(prompt, view, selPoint)
End Function

End Module

This is absolutely brilliant. However, with the DraftingCurve introduction in later versions of NX this does not seem to work anymore. The only thing I could get it to work on was pre-nx8 views on the silhouette curves. This would be a god send if it worked for NX8.5+.
The DraftingCurve.OwningPart gives the actual drawing sheet and the OwningComponent gives a null tag.

I only have access to NX7.5 at the moment. We are due to upgrade to NX10 later this year. In the meantime, I'd be happy to work on it with you if you can test it.

Hello Ian, Have you found how to use the method .MapModelToDrawing. Do you have any example for this method or a syntax for this method. I want to learn this method as i'am working on positioning the point label w.r.t the point itself. Thanks, B

Balaji

Hi guys! Any news about this with nx9 or later?

I now have access to NX10, so I should be able to work on this again. I'll let you know when I make some progress.

Have you been able to update it to make it work in NX10?

thanks!I´ve been looking for this for long. I´m cluless about programming, so I can´t do nothing but ask whoever it takes (because the way it works now is not proficient...).

Hello,
I find this utility very interesting and very useful!

However, I use NX11 and it does not seem to work on this version. That is true ?

Would it be possible to update this code to work with NX11?

Best Regards !

Pat

Hello,

It's possible to create a smart balloon without any part-list. Let me explain, I would like to create a balloon manually attached to a component (edge ​​or surface) and the number displayed in the balloon must match the callout attribute of the coin it is associated with? I wish to be able to insert more than one balloon for the same component if it is found in several copies in an assembly. Unless I'm wrong, it's not possible to do that in NX11 and NX12, you have to manually enter the balloon number if you have more than one balloon with the same number, but I would like do not have to manually enter the number in the ball.

In addition, could the number displayed in the balloon can be automatically updated if Callout changes?

It's possible to do this with the previous examples? If so, how can you help me?

Best!

Pat

Hi,

Unfortunately, I have no way of testing this journal in either NX11 or NX12. So, unless someone else can provide an update, it will need to wait until my company (Renishaw) upgrade from NX10. Sorry!

Ian