{"TotalCount":19,"Files":[{"Ident":"bluedock.modelpro","Path":"Editor/VmdlEditTab.cs","FileName":"VmdlEditTab.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340317,"Code":"using System;\nusing System.Globalization;\nusing System.IO;\nusing ModelPro.Vmdl;\n\nnamespace ModelPro.Editor;\n\n/// \u003Csummary\u003E\n/// The VMDL tab of Model Pro. Pick a folder, it recursively finds every .vmdl\n/// file, and you can apply uniform scale and align-origin X/Y/Z changes to all\n/// their mesh entries at once.\n/// \u003C/summary\u003E\npublic class VmdlEditTab : Widget\n{\n\tTreeView _folderTree;\n\tTreeView _fileList;\n\tLabel _statusLabel;\n\n\tLineEdit _scaleEdit;\n\tComboBox _scaleUnit;\n\tScaleUnit _lastScaleUnit = ScaleUnit.Inches;\n\tComboBox _alignX;\n\tComboBox _alignY;\n\tComboBox _alignZ;\n\tControlSheet _materialSheet;\n\tDefaultMaterialModel _materialModel = new();\n\tButton _applyButton;\n\tLabel _resultLabel;\n\n\tList\u003Cstring\u003E _foundVmdlFiles = new();\n\n\tpublic VmdlEditTab( Widget parent ) : base( parent )\n\t{\n\t\tBuildUI();\n\t\tLoadSettings();\n\t}\n\n\tprivate void BuildUI()\n\t{\n\t\tLayout = Layout.Column();\n\t\tLayout.Spacing = 4;\n\n\t\tvar split = Layout.AddRow();\n\t\tsplit.Spacing = 8;\n\n\t\tvar left = split.AddColumn();\n\t\tleft.Spacing = 4;\n\t\tleft.Add( new Label( \u0022\u003Cb\u003EFolder\u003C/b\u003E\u0022 ) );\n\n\t\tvar treeScroll = left.Add( new ScrollArea( this ), 1 );\n\t\ttreeScroll.Canvas = new Widget();\n\t\ttreeScroll.Canvas.Layout = Layout.Column();\n\t\ttreeScroll.Canvas.Layout.Margin = new Sandbox.UI.Margin( 2 );\n\n\t\t_folderTree = new TreeView( treeScroll.Canvas );\n\t\ttreeScroll.Canvas.Layout.Add( _folderTree );\n\t\t_folderTree.ExpandForSelection = true;\n\n\t\tAddFolderNodes();\n\n\t\tvar right = split.AddColumn();\n\t\tright.Spacing = 4;\n\n\t\t_statusLabel = right.Add( new Label( \u0022Select a folder to search for models.\u0022 ) );\n\t\t_statusLabel.WordWrap = true;\n\n\t\tvar listScroll = right.Add( new ScrollArea( this ), 1 );\n\t\tlistScroll.Canvas = new Widget();\n\t\tlistScroll.Canvas.Layout = Layout.Column();\n\t\tlistScroll.Canvas.Layout.Margin = new Sandbox.UI.Margin( 2 );\n\n\t\t_fileList = new TreeView( listScroll.Canvas );\n\t\tlistScroll.Canvas.Layout.Add( _fileList );\n\n\t\tvar group = right.Add( new Widget() );\n\t\tvar grid = Layout.Grid();\n\t\tgrid.Spacing = 4;\n\t\tgroup.Layout = grid;\n\n\t\tint row = 0;\n\n\t\tgrid.AddCell( 0, row, new Label( \u0022Scale\u0022 ) );\n\n\t\tvar scaleRow = Layout.Row();\n\t\tscaleRow.Spacing = 4;\n\t\t_scaleEdit = new LineEdit() { Text = \u00221.0\u0022, FixedWidth = 90 };\n\t\t_scaleUnit = new ComboBox() { FixedWidth = 130 };\n\t\tforeach ( var unit in Enum.GetValues\u003CScaleUnit\u003E() )\n\t\t{\n\t\t\tvar u = unit;\n\t\t\t_scaleUnit.AddItem( u.ToDisplayString(), onSelected: () =\u003E OnScaleUnitChanged( u ) );\n\t\t}\n\t\t_scaleUnit.TrySelectNamed( ScaleUnit.Inches.ToDisplayString() );\n\t\tscaleRow.Add( _scaleEdit );\n\t\tscaleRow.Add( _scaleUnit );\n\t\tgrid.AddCell( 1, row, scaleRow );\n\t\trow\u002B\u002B;\n\n\t\tgrid.AddCell( 0, row, new Label( \u0022Align Origin X\u0022 ) );\n\t\t_alignX = CreateAlignCombo();\n\t\tgrid.AddCell( 1, row, _alignX );\n\t\trow\u002B\u002B;\n\n\t\tgrid.AddCell( 0, row, new Label( \u0022Align Origin Y\u0022 ) );\n\t\t_alignY = CreateAlignCombo();\n\t\tgrid.AddCell( 1, row, _alignY );\n\t\trow\u002B\u002B;\n\n\t\tgrid.AddCell( 0, row, new Label( \u0022Align Origin Z\u0022 ) );\n\t\t_alignZ = CreateAlignCombo();\n\t\tgrid.AddCell( 1, row, _alignZ );\n\t\trow\u002B\u002B;\n\n\t\t// The material picker has its own \u0022Material\u0022 label, so give it a fixed\n\t\t// width row below the grid - otherwise it stretches the whole window.\n\t\tvar materialWrap = new Widget() { FixedWidth = 280 };\n\t\tmaterialWrap.Layout = Layout.Column();\n\t\t_materialSheet = new ControlSheet();\n\t\t_materialSheet.AddProperty( _materialModel, x =\u003E x.Material );\n\t\tmaterialWrap.Layout.Add( _materialSheet );\n\t\tright.Add( materialWrap );\n\n\t\t_applyButton = new Button( \u0022Apply to N models\u0022, \u0022check\u0022 );\n\t\t_applyButton.Clicked \u002B= ApplyEdits;\n\t\tright.Add( _applyButton );\n\n\t\t_resultLabel = new Label( \u0022\u0022 );\n\t\t_resultLabel.WordWrap = true;\n\t\tright.Add( _resultLabel );\n\t}\n\n\tprivate ComboBox CreateAlignCombo()\n\t{\n\t\tvar combo = new ComboBox() { FixedWidth = 120 };\n\t\tcombo.AddItem( \u0022None\u0022 );\n\t\tcombo.AddItem( \u0022BoundsCenter\u0022 );\n\t\tcombo.AddItem( \u0022BoundsMin\u0022 );\n\t\tcombo.AddItem( \u0022BoundsMax\u0022 );\n\t\tcombo.TrySelectNamed( \u0022None\u0022 );\n\t\treturn combo;\n\t}\n\n\t/// \u003Csummary\u003ERestore the last used values into the controls.\u003C/summary\u003E\n\tprivate void LoadSettings()\n\t{\n\t\tvar s = ModelProSettings.Current;\n\n\t\t_scaleEdit.Text = s.VmdlScale;\n\t\t_lastScaleUnit = s.VmdlScaleUnit;\n\t\t_scaleUnit.TrySelectNamed( s.VmdlScaleUnit.ToDisplayString() );\n\t\t_alignX.TrySelectNamed( s.VmdlAlignX.ToKv3Value() );\n\t\t_alignY.TrySelectNamed( s.VmdlAlignY.ToKv3Value() );\n\t\t_alignZ.TrySelectNamed( s.VmdlAlignZ.ToKv3Value() );\n\t\t_materialModel.Material = s.VmdlDefaultMaterial;\n\t}\n\n\t/// \u003Csummary\u003ERemember the current values for next time.\u003C/summary\u003E\n\tprivate void SaveSettings()\n\t{\n\t\tvar s = ModelProSettings.Current;\n\t\ts.VmdlScale = _scaleEdit.Text;\n\t\ts.VmdlScaleUnit = ParseScaleUnit( _scaleUnit.CurrentText );\n\t\ts.VmdlAlignX = ParseAlign( _alignX.CurrentText ).Value;\n\t\ts.VmdlAlignY = ParseAlign( _alignY.CurrentText ).Value;\n\t\ts.VmdlAlignZ = ParseAlign( _alignZ.CurrentText ).Value;\n\t\ts.VmdlDefaultMaterial = _materialModel.Material ?? \u0022\u0022;\n\t\ts.Save();\n\t}\n\n\tprivate void AddFolderNodes()\n\t{\n\t\tvar rootDir = Sandbox.Project.Current?.RootDirectory;\n\t\tif ( rootDir is null )\n\t\t\treturn;\n\n\t\tvar root = new FolderNode( rootDir.FullName, OnFolderSelected );\n\t\t_folderTree.AddItem( root );\n\t\t_folderTree.Open( root );\n\t}\n\n\tprivate void OnFolderSelected( string folder )\n\t{\n\t\t_statusLabel.Text = $\u0022Searching \u003Cb\u003E{folder}\u003C/b\u003E...\u0022;\n\t\t_resultLabel.Text = \u0022\u0022;\n\n\t\t_foundVmdlFiles = Directory.GetFiles( folder, \u0022*.vmdl\u0022, SearchOption.AllDirectories )\n\t\t\t.OrderBy( x =\u003E x, StringComparer.OrdinalIgnoreCase )\n\t\t\t.ToList();\n\n\t\t_fileList.Clear();\n\n\t\tint meshEntryTotal = 0;\n\t\tforeach ( var file in _foundVmdlFiles )\n\t\t{\n\t\t\tint count = 0;\n\t\t\ttry\n\t\t\t{\n\t\t\t\tvar editor = VmdlBulkEditor.LoadFile( file );\n\t\t\t\tcount = editor.MeshEntryCount;\n\t\t\t\tmeshEntryTotal \u002B= count;\n\t\t\t}\n\t\t\tcatch ( Exception e )\n\t\t\t{\n\t\t\t\tLog.Warning( e, $\u0022Model Pro: failed to parse {file}\u0022 );\n\t\t\t}\n\n\t\t\t_fileList.AddItem( new FileNode( file, count ) );\n\t\t}\n\n\t\t_statusLabel.Text = $\u0022Found \u003Cb\u003E{_foundVmdlFiles.Count}\u003C/b\u003E models in \u003Cb\u003E{folder}\u003C/b\u003E \u2014 {meshEntryTotal} mesh entries.\u0022;\n\n\t\t_applyButton.Text = _foundVmdlFiles.Count == 0\n\t\t\t? \u0022Apply to 0 models\u0022\n\t\t\t: $\u0022Apply to {_foundVmdlFiles.Count} models\u0022;\n\t}\n\n\tprivate void ApplyEdits()\n\t{\n\t\tif ( _foundVmdlFiles.Count == 0 )\n\t\t\treturn;\n\n\t\tvar props = new MeshEntryProperties\n\t\t{\n\t\t\tImportScale = ReadScale(),\n\t\t\tAlignOriginX = ParseAlign( _alignX.CurrentText ),\n\t\t\tAlignOriginY = ParseAlign( _alignY.CurrentText ),\n\t\t\tAlignOriginZ = ParseAlign( _alignZ.CurrentText ),\n\t\t\tGlobalDefaultMaterial = string.IsNullOrWhiteSpace( _materialModel.Material ) ? null : _materialModel.Material.Trim()\n\t\t};\n\n\t\tLog.Info( $\u0022Model Pro: applying scale={props.ImportScale?.ToString( \u00220.0########\u0022, CultureInfo.InvariantCulture ) ?? \u0022off\u0022} \u0022 \u002B\n\t\t\t$\u0022align=({_alignX.CurrentText},{_alignY.CurrentText},{_alignZ.CurrentText}) to {_foundVmdlFiles.Count} file(s)\u0022 );\n\n\t\tint modified = 0;\n\t\tint totalEntries = 0;\n\t\tint noMeshEntries = 0;\n\t\tint alreadyMatched = 0;\n\t\tvar errors = new List\u003Cstring\u003E();\n\n\t\tforeach ( var file in _foundVmdlFiles )\n\t\t{\n\t\t\ttry\n\t\t\t{\n\t\t\t\tvar editor = VmdlBulkEditor.LoadFile( file );\n\t\t\t\tif ( editor.MeshEntryCount == 0 )\n\t\t\t\t{\n\t\t\t\t\tnoMeshEntries\u002B\u002B;\n\t\t\t\t\tLog.Info( $\u0022Model Pro: {Path.GetFileName( file )} - no RenderMeshFile entries found\u0022 );\n\t\t\t\t}\n\t\t\t\telse if ( editor.Apply( props ) )\n\t\t\t\t{\n\t\t\t\t\tFile.WriteAllText( file, editor.Source );\n\t\t\t\t\tmodified\u002B\u002B;\n\t\t\t\t\tLog.Info( $\u0022Model Pro: updated {Path.GetFileName( file )}\u0022 );\n\n\t\t\t\t\tRecompileModel( file );\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\talreadyMatched\u002B\u002B;\n\t\t\t\t\tLog.Info( $\u0022Model Pro: {Path.GetFileName( file )} - values already match\u0022 );\n\t\t\t\t}\n\n\t\t\t\ttotalEntries \u002B= editor.MeshEntryCount;\n\t\t\t}\n\t\t\tcatch ( Exception e )\n\t\t\t{\n\t\t\t\terrors.Add( $\u0022{Path.GetFileName( file )}: {e.Message}\u0022 );\n\t\t\t\tLog.Warning( e, $\u0022Model Pro: failed to process {file}\u0022 );\n\t\t\t}\n\t\t}\n\n\t\tvar msg = $\u0022Updated \u003Cb\u003E{modified}\u003C/b\u003E of {_foundVmdlFiles.Count} models ({totalEntries} mesh entries).\u0022;\n\t\tif ( modified == 0 \u0026\u0026 errors.Count == 0 )\n\t\t{\n\t\t\tvar reason = noMeshEntries \u003E 0 \u0026\u0026 alreadyMatched == 0\n\t\t\t\t? \u0022None of the files contain RenderMeshFile entries.\u0022\n\t\t\t\t: \u0022The values already match what\u0027s set above. Change a value and try again.\u0022;\n\t\t\tmsg = $\u0022No models needed updating \u2014 {reason}\u0022;\n\t\t}\n\n\t\tif ( errors.Count \u003E 0 )\n\t\t\tmsg \u002B= $\u0022\\n\\nErrors ({errors.Count}):\\n{string.Join( \u0022\\n\u0022, errors.Take( 8 ) )}\u0022;\n\n\t\t_resultLabel.Text = msg;\n\n\t\tSaveSettings();\n\t}\n\n\tprivate static AlignOrigin? ParseAlign( string text )\n\t{\n\t\treturn text switch\n\t\t{\n\t\t\t\u0022Center\u0022 =\u003E AlignOrigin.Center,\n\t\t\t\u0022Mins\u0022 =\u003E AlignOrigin.Mins,\n\t\t\t\u0022Maxs\u0022 =\u003E AlignOrigin.Maxs,\n\t\t\t\u0022BoundsCenter\u0022 =\u003E AlignOrigin.BoundsCenter,\n\t\t\t\u0022BoundsMin\u0022 =\u003E AlignOrigin.BoundsMin,\n\t\t\t\u0022BoundsMax\u0022 =\u003E AlignOrigin.BoundsMax,\n\t\t\t_ =\u003E AlignOrigin.None\n\t\t};\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Force the editor to recompile the model from its (just-written) source file\n\t/// so the asset system and any open ModelDoc pick up the change.\n\t/// \u003C/summary\u003E\n\tprivate static void RecompileModel( string absolutePath )\n\t{\n\t\ttry\n\t\t{\n\t\t\tvar project = Sandbox.Project.Current;\n\t\t\tvar assetsRoot = project?.GetAssetsPath();\n\t\t\tif ( string.IsNullOrEmpty( assetsRoot ) )\n\t\t\t\treturn;\n\n\t\t\t// Find the asset by its content-relative path - FindByPath expects the\n\t\t\t// relative asset path (e.g. \u0022models/vehicle_01_a.vmdl\u0022), not a Windows path.\n\t\t\tvar relative = absolutePath.Replace( \u0027\\\\\u0027, \u0027/\u0027 );\n\t\t\tif ( relative.StartsWith( assetsRoot.Replace( \u0027\\\\\u0027, \u0027/\u0027 ), StringComparison.OrdinalIgnoreCase ) )\n\t\t\t\trelative = relative.Substring( assetsRoot.Replace( \u0027\\\\\u0027, \u0027/\u0027 ).Length ).TrimStart( \u0027/\u0027 );\n\n\t\t\tvar asset = AssetSystem.FindByPath( relative );\n\t\t\tif ( asset is null )\n\t\t\t{\n\t\t\t\t// Fall back to matching by absolute path.\n\t\t\t\tasset = AssetSystem.FindByPath( absolutePath );\n\t\t\t}\n\n\t\t\tif ( asset is null )\n\t\t\t{\n\t\t\t\tLog.Warning( $\u0022Model Pro: couldn\u0027t find asset for \u0027{absolutePath}\u0027 to recompile\u0022 );\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tasset.Compile( true );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( e, $\u0022Model Pro: failed to recompile \u0027{absolutePath}\u0027\u0022 );\n\t\t}\n\t}\n\n\tprivate float? ReadScale()\n\t{\n\t\tif ( !float.TryParse( _scaleEdit.Text, NumberStyles.Float, CultureInfo.InvariantCulture, out var value ) )\n\t\t\treturn null;\n\n\t\tvar unit = ParseScaleUnit( _scaleUnit.CurrentText );\n\t\treturn new ScaleInput { Value = value, Unit = unit }.ToImportScale();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// When the unit dropdown changes, convert the current scale value so the\n\t/// import scale stays the same - just like the create-model popup does.\n\t/// \u003C/summary\u003E\n\tprivate void OnScaleUnitChanged( ScaleUnit newUnit )\n\t{\n\t\tif ( newUnit == _lastScaleUnit )\n\t\t\treturn;\n\n\t\tif ( float.TryParse( _scaleEdit.Text, NumberStyles.Float, CultureInfo.InvariantCulture, out var current ) )\n\t\t{\n\t\t\tvar converted = new ScaleInput { Value = current, Unit = _lastScaleUnit }.ConvertTo( newUnit );\n\t\t\t_scaleEdit.Text = converted.Value.ToString( \u00220.0########\u0022, CultureInfo.InvariantCulture );\n\t\t}\n\n\t\t_lastScaleUnit = newUnit;\n\t}\n\n\tprivate static ScaleUnit ParseScaleUnit( string text )\n\t{\n\t\tforeach ( var unit in Enum.GetValues\u003CScaleUnit\u003E() )\n\t\t{\n\t\t\tif ( string.Equals( unit.ToDisplayString(), text, StringComparison.OrdinalIgnoreCase ) )\n\t\t\t\treturn unit;\n\t\t}\n\n\t\treturn ScaleUnit.Inches;\n\t}\n}\n"},{"Ident":"bluedock.modelpro","Path":"Editor/Vmdl/Kv3Parser.cs","FileName":"Kv3Parser.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340317,"Code":"using System;\nusing System.Text;\n\nnamespace ModelPro.Vmdl;\n\ninternal enum Kv3TokenType\n{\n\tLBrace,\n\tRBrace,\n\tLBracket,\n\tRBracket,\n\tEquals,\n\tComma,\n\tString,\n\tIdent,\n\tEnd\n}\n\ninternal readonly struct Kv3Token\n{\n\tpublic readonly Kv3TokenType Type;\n\tpublic readonly int Start;\n\tpublic readonly int End;\n\tpublic readonly string Text;\n\n\tpublic Kv3Token( Kv3TokenType type, int start, int end, string text )\n\t{\n\t\tType = type;\n\t\tStart = start;\n\t\tEnd = end;\n\t\tText = text;\n\t}\n}\n\n/// \u003Csummary\u003E\n/// Tokenizes KV3 text (the format model .vmdl source files are stored in).\n/// Handles the \u003C!-- ... --\u003E header comment, // and /* */ comments, quoted strings\n/// with escapes, and barewords (identifiers and numbers).\n/// \u003C/summary\u003E\ninternal sealed class Kv3Tokenizer\n{\n\treadonly string src;\n\tint pos;\n\n\tpublic Kv3Tokenizer( string src )\n\t{\n\t\tthis.src = src;\n\t}\n\n\tpublic List\u003CKv3Token\u003E Tokenize()\n\t{\n\t\tvar tokens = new List\u003CKv3Token\u003E();\n\n\t\twhile ( true )\n\t\t{\n\t\t\tSkipTrivia();\n\t\t\tif ( pos \u003E= src.Length )\n\t\t\t{\n\t\t\t\ttokens.Add( new Kv3Token( Kv3TokenType.End, pos, pos, \u0022\u0022 ) );\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tint start = pos;\n\t\t\tchar c = src[pos];\n\n\t\t\tswitch ( c )\n\t\t\t{\n\t\t\t\tcase \u0027{\u0027:\n\t\t\t\t\tpos\u002B\u002B;\n\t\t\t\t\ttokens.Add( new Kv3Token( Kv3TokenType.LBrace, start, pos, \u0022{\u0022 ) );\n\t\t\t\t\tbreak;\n\t\t\t\tcase \u0027}\u0027:\n\t\t\t\t\tpos\u002B\u002B;\n\t\t\t\t\ttokens.Add( new Kv3Token( Kv3TokenType.RBrace, start, pos, \u0022}\u0022 ) );\n\t\t\t\t\tbreak;\n\t\t\t\tcase \u0027[\u0027:\n\t\t\t\t\tpos\u002B\u002B;\n\t\t\t\t\ttokens.Add( new Kv3Token( Kv3TokenType.LBracket, start, pos, \u0022[\u0022 ) );\n\t\t\t\t\tbreak;\n\t\t\t\tcase \u0027]\u0027:\n\t\t\t\t\tpos\u002B\u002B;\n\t\t\t\t\ttokens.Add( new Kv3Token( Kv3TokenType.RBracket, start, pos, \u0022]\u0022 ) );\n\t\t\t\t\tbreak;\n\t\t\t\tcase \u0027=\u0027:\n\t\t\t\t\tpos\u002B\u002B;\n\t\t\t\t\ttokens.Add( new Kv3Token( Kv3TokenType.Equals, start, pos, \u0022=\u0022 ) );\n\t\t\t\t\tbreak;\n\t\t\t\tcase \u0027,\u0027:\n\t\t\t\t\tpos\u002B\u002B;\n\t\t\t\t\ttokens.Add( new Kv3Token( Kv3TokenType.Comma, start, pos, \u0022,\u0022 ) );\n\t\t\t\t\tbreak;\n\t\t\t\tcase \u0027\u0022\u0027:\n\t\t\t\t\tReadString( tokens, start );\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tReadBareword( tokens, start );\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\treturn tokens;\n\t}\n\n\tvoid ReadString( List\u003CKv3Token\u003E tokens, int start )\n\t{\n\t\tpos\u002B\u002B; // opening quote\n\t\twhile ( pos \u003C src.Length \u0026\u0026 src[pos] != \u0027\u0022\u0027 )\n\t\t{\n\t\t\tif ( src[pos] == \u0027\\\\\u0027 \u0026\u0026 pos \u002B 1 \u003C src.Length )\n\t\t\t\tpos \u002B= 2;\n\t\t\telse\n\t\t\t\tpos\u002B\u002B;\n\t\t}\n\n\t\tif ( pos \u003C src.Length )\n\t\t\tpos\u002B\u002B; // closing quote\n\n\t\ttokens.Add( new Kv3Token( Kv3TokenType.String, start, pos, src.Substring( start, pos - start ) ) );\n\t}\n\n\tvoid ReadBareword( List\u003CKv3Token\u003E tokens, int start )\n\t{\n\t\twhile ( pos \u003C src.Length )\n\t\t{\n\t\t\tchar c = src[pos];\n\t\t\tif ( char.IsWhiteSpace( c ) || c is \u0027{\u0027 or \u0027}\u0027 or \u0027[\u0027 or \u0027]\u0027 or \u0027=\u0027 or \u0027,\u0027 )\n\t\t\t\tbreak;\n\t\t\tif ( c == \u0027/\u0027 \u0026\u0026 pos \u002B 1 \u003C src.Length \u0026\u0026 src[pos \u002B 1] is \u0027/\u0027 or \u0027*\u0027 )\n\t\t\t\tbreak;\n\t\t\tif ( c == \u0027\u003C\u0027 \u0026\u0026 pos \u002B 3 \u003C src.Length \u0026\u0026 src.AsSpan( pos ).StartsWith( \u0022\u003C!--\u0022 ) )\n\t\t\t\tbreak;\n\t\t\tpos\u002B\u002B;\n\t\t}\n\n\t\ttokens.Add( new Kv3Token( Kv3TokenType.Ident, start, pos, src.Substring( start, pos - start ) ) );\n\t}\n\n\tvoid SkipTrivia()\n\t{\n\t\twhile ( pos \u003C src.Length )\n\t\t{\n\t\t\tchar c = src[pos];\n\n\t\t\tif ( char.IsWhiteSpace( c ) )\n\t\t\t{\n\t\t\t\tpos\u002B\u002B;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ( c == \u0027/\u0027 \u0026\u0026 pos \u002B 1 \u003C src.Length \u0026\u0026 src[pos \u002B 1] == \u0027/\u0027 )\n\t\t\t{\n\t\t\t\tpos \u002B= 2;\n\t\t\t\twhile ( pos \u003C src.Length \u0026\u0026 src[pos] != \u0027\\n\u0027 )\n\t\t\t\t\tpos\u002B\u002B;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ( c == \u0027/\u0027 \u0026\u0026 pos \u002B 1 \u003C src.Length \u0026\u0026 src[pos \u002B 1] == \u0027*\u0027 )\n\t\t\t{\n\t\t\t\tvar end = src.IndexOf( \u0022*/\u0022, pos \u002B 2, StringComparison.Ordinal );\n\t\t\t\tpos = end \u003C 0 ? src.Length : end \u002B 2;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ( c == \u0027\u003C\u0027 \u0026\u0026 pos \u002B 3 \u003C src.Length \u0026\u0026 src.AsSpan( pos ).StartsWith( \u0022\u003C!--\u0022 ) )\n\t\t\t{\n\t\t\t\tvar end = src.IndexOf( \u0022--\u003E\u0022, pos, StringComparison.Ordinal );\n\t\t\t\tpos = end \u003C 0 ? src.Length : end \u002B 3;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\n/// \u003Csummary\u003EA parsed node in a KV3 document.\u003C/summary\u003E\npublic abstract class Kv3Node\n{\n\tpublic int Start;\n\tpublic int End;\n}\n\n/// \u003Csummary\u003EAn object - a list of key/value fields.\u003C/summary\u003E\npublic sealed class Kv3Object : Kv3Node\n{\n\tpublic List\u003CKv3Field\u003E Fields = new();\n\n\tpublic Kv3Field FindField( string key ) =\u003E Fields.FirstOrDefault( f =\u003E f.Key == key );\n\n\t/// \u003Csummary\u003EThe value of the \u0022_class\u0022 field, used to identify node types in modeldoc files.\u003C/summary\u003E\n\tpublic string Class =\u003E (FindField( \u0022_class\u0022 )?.Value as Kv3Scalar)?.Value;\n}\n\npublic sealed class Kv3Field\n{\n\tpublic string Key;\n\tpublic int KeyStart;\n\tpublic int KeyEnd;\n\tpublic Kv3Node Value;\n}\n\n/// \u003Csummary\u003EAn array of values.\u003C/summary\u003E\npublic sealed class Kv3Array : Kv3Node\n{\n\tpublic List\u003CKv3Node\u003E Items = new();\n}\n\n/// \u003Csummary\u003EA scalar value - a string, number, or bareword.\u003C/summary\u003E\npublic sealed class Kv3Scalar : Kv3Node\n{\n\tpublic bool IsString;\n\tpublic string Raw;\n\tpublic string Value;\n}\n\n/// \u003Csummary\u003E\n/// A parsed KV3 text document. Keeps a reference to the original source so edits\n/// can be applied surgically without reformatting the whole file.\n/// \u003C/summary\u003E\npublic sealed class Kv3Document\n{\n\tpublic string Source { get; }\n\tpublic Kv3Object Root { get; }\n\n\tKv3Document( string source )\n\t{\n\t\tSource = source;\n\t\tRoot = Kv3Parser.Parse( source );\n\t}\n\n\tpublic static Kv3Document Parse( string source ) =\u003E new( source );\n\n\t/// \u003Csummary\u003E\n\t/// Walk the tree and return every object whose \u0022_class\u0022 field matches the given name.\n\t/// \u003C/summary\u003E\n\tpublic List\u003CKv3Object\u003E FindObjects( string className )\n\t{\n\t\tvar result = new List\u003CKv3Object\u003E();\n\t\tWalk( Root, result, className );\n\t\treturn result;\n\t}\n\n\tstatic void Walk( Kv3Node node, List\u003CKv3Object\u003E result, string className )\n\t{\n\t\tif ( node is Kv3Object obj )\n\t\t{\n\t\t\tif ( obj.Class == className )\n\t\t\t\tresult.Add( obj );\n\n\t\t\tforeach ( var f in obj.Fields )\n\t\t\t\tWalk( f.Value, result, className );\n\t\t}\n\t\telse if ( node is Kv3Array arr )\n\t\t{\n\t\t\tforeach ( var item in arr.Items )\n\t\t\t\tWalk( item, result, className );\n\t\t}\n\t}\n}\n\ninternal sealed class Kv3Parser\n{\n\treadonly string src;\n\treadonly List\u003CKv3Token\u003E tokens;\n\tint index;\n\n\tKv3Parser( string src, List\u003CKv3Token\u003E tokens )\n\t{\n\t\tthis.src = src;\n\t\tthis.tokens = tokens;\n\t}\n\n\tpublic static Kv3Object Parse( string source )\n\t{\n\t\tvar tokens = new Kv3Tokenizer( source ).Tokenize();\n\t\tvar parser = new Kv3Parser( source, tokens );\n\t\treturn parser.ParseObject();\n\t}\n\n\tKv3Token Peek =\u003E tokens[index];\n\tKv3Token Next() =\u003E tokens[index\u002B\u002B];\n\tbool AtEnd =\u003E Peek.Type == Kv3TokenType.End;\n\n\tKv3Node ParseValue()\n\t{\n\t\tvar tok = Peek;\n\t\tif ( tok.Type == Kv3TokenType.LBrace )\n\t\t\treturn ParseObject();\n\t\tif ( tok.Type == Kv3TokenType.LBracket )\n\t\t\treturn ParseArray();\n\t\treturn ParseScalar();\n\t}\n\n\tKv3Object ParseObject()\n\t{\n\t\tvar open = Next(); // {\n\t\tvar obj = new Kv3Object { Start = open.Start };\n\n\t\twhile ( true )\n\t\t{\n\t\t\tif ( AtEnd )\n\t\t\t\tbreak;\n\n\t\t\tif ( Peek.Type == Kv3TokenType.RBrace )\n\t\t\t{\n\t\t\t\tvar close = Next();\n\t\t\t\tobj.End = close.End;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif ( Peek.Type == Kv3TokenType.Comma )\n\t\t\t{\n\t\t\t\tNext();\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tvar keyTok = Peek;\n\t\t\tif ( keyTok.Type is not (Kv3TokenType.String or Kv3TokenType.Ident) )\n\t\t\t{\n\t\t\t\tNext();\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tNext(); // consume key\n\n\t\t\tstring key = keyTok.Type == Kv3TokenType.String\n\t\t\t\t? Unquote( keyTok.Text )\n\t\t\t\t: keyTok.Text;\n\n\t\t\tif ( Peek.Type == Kv3TokenType.Equals )\n\t\t\t\tNext();\n\n\t\t\tvar value = ParseValue();\n\t\t\tobj.Fields.Add( new Kv3Field { Key = key, KeyStart = keyTok.Start, KeyEnd = keyTok.End, Value = value } );\n\t\t}\n\n\t\treturn obj;\n\t}\n\n\tKv3Array ParseArray()\n\t{\n\t\tvar open = Next(); // [\n\t\tvar arr = new Kv3Array { Start = open.Start };\n\n\t\twhile ( true )\n\t\t{\n\t\t\tif ( AtEnd )\n\t\t\t\tbreak;\n\n\t\t\tif ( Peek.Type == Kv3TokenType.RBracket )\n\t\t\t{\n\t\t\t\tvar close = Next();\n\t\t\t\tarr.End = close.End;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif ( Peek.Type == Kv3TokenType.Comma )\n\t\t\t{\n\t\t\t\tNext();\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tarr.Items.Add( ParseValue() );\n\t\t}\n\n\t\treturn arr;\n\t}\n\n\tKv3Scalar ParseScalar()\n\t{\n\t\tvar tok = Next();\n\t\treturn new Kv3Scalar\n\t\t{\n\t\t\tStart = tok.Start,\n\t\t\tEnd = tok.End,\n\t\t\tIsString = tok.Type == Kv3TokenType.String,\n\t\t\tRaw = tok.Text,\n\t\t\tValue = tok.Type == Kv3TokenType.String ? Unquote( tok.Text ) : tok.Text\n\t\t};\n\t}\n\n\tstatic string Unquote( string s )\n\t{\n\t\tif ( s.Length \u003C 2 || s[0] != \u0027\u0022\u0027 )\n\t\t\treturn s;\n\n\t\tvar inner = s.Substring( 1, s.Length - 2 );\n\t\tvar sb = new StringBuilder( inner.Length );\n\n\t\tfor ( int i = 0; i \u003C inner.Length; i\u002B\u002B )\n\t\t{\n\t\t\tif ( inner[i] == \u0027\\\\\u0027 \u0026\u0026 i \u002B 1 \u003C inner.Length )\n\t\t\t{\n\t\t\t\tchar n = inner[\u002B\u002Bi];\n\t\t\t\tswitch ( n )\n\t\t\t\t{\n\t\t\t\t\tcase \u0027n\u0027:\n\t\t\t\t\t\tsb.Append( \u0027\\n\u0027 );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \u0027t\u0027:\n\t\t\t\t\t\tsb.Append( \u0027\\t\u0027 );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \u0027r\u0027:\n\t\t\t\t\t\tsb.Append( \u0027\\r\u0027 );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \u0027\u0022\u0027:\n\t\t\t\t\t\tsb.Append( \u0027\u0022\u0027 );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \u0027\\\u0027\u0027:\n\t\t\t\t\t\tsb.Append( \u0027\\\u0027\u0027 );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \u0027\\\\\u0027:\n\t\t\t\t\t\tsb.Append( \u0027\\\\\u0027 );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tsb.Append( n );\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tsb.Append( inner[i] );\n\t\t\t}\n\t\t}\n\n\t\treturn sb.ToString();\n\t}\n}\n"},{"Ident":"bluedock.modelpro","Path":"Code/Assembly.cs","FileName":"Assembly.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":340317,"Code":"global using Sandbox;\nglobal using System.Collections.Generic;\nglobal using System.Linq;\n"},{"Ident":"bluedock.modelpro","Path":"Editor/DefaultMaterialModel.cs","FileName":"DefaultMaterialModel.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340317,"Code":"using System;\nusing System.ComponentModel;\n\nnamespace ModelPro.Editor;\n\n/// \u003Csummary\u003E\n/// A concrete \u003Csee cref=\u0022AssetPathAttribute\u0022/\u003E that restricts picking to a single\n/// asset type extension. Used so the material field only accepts .vmat files.\n/// \u003C/summary\u003E\n[AttributeUsage( AttributeTargets.Property )]\npublic class AssetPathTypeAttribute : AssetPathAttribute\n{\n\treadonly string _extension;\n\n\tpublic AssetPathTypeAttribute( string extension )\n\t{\n\t\t_extension = extension;\n\t}\n\n\tpublic override string AssetTypeExtension =\u003E _extension;\n}\n\n/// \u003Csummary\u003E\n/// A tiny serialized object used to render a material picker control. The\n/// [AssetPathType(\u0022vmat\u0022)] attribute makes the editor use its material picker,\n/// so only .vmat assets can be selected.\n/// \u003C/summary\u003E\npublic class DefaultMaterialModel\n{\n\t/// \u003Csummary\u003EThe selected default material path, e.g. \u0022materials/default.vmat\u0022.\u003C/summary\u003E\n\t[AssetPathType( \u0022vmat\u0022 )]\n\tpublic string Material { get; set; } = \u0022\u0022;\n}\n"},{"Ident":"bluedock.modelpro","Path":"UnitTests/VmdlGeneratorTests.cs","FileName":"VmdlGeneratorTests.cs","PackageType":"library","CodeKind":"UnitTest","AssetVersionId":340317,"Code":"using System;\nusing System.Linq;\nusing Microsoft.VisualStudio.TestTools.UnitTesting;\nusing ModelPro.Vmdl;\n\n[TestClass]\npublic class VmdlGeneratorTests\n{\n\t[TestMethod]\n\tpublic void GeneratesValidVmdl_HullCollision()\n\t{\n\t\tvar options = new MeshToModelOptions\n\t\t{\n\t\t\tCollision = CollisionMode.Hull,\n\t\t\tImportScale = 0.3937f,\n\t\t\tAlignOriginX = AlignOrigin.BoundsCenter,\n\t\t\tAlignOriginY = AlignOrigin.BoundsCenter,\n\t\t\tAlignOriginZ = AlignOrigin.BoundsMin\n\t\t};\n\n\t\tvar source = VmdlGenerator.Generate( \u0022models/foo.fbx\u0022, options );\n\n\t\t// Parseable, finds all nodes.\n\t\tvar doc = Kv3Document.Parse( source );\n\t\tAssert.AreEqual( 1, doc.FindObjects( \u0022RenderMeshFile\u0022 ).Count );\n\t\tAssert.AreEqual( 1, doc.FindObjects( \u0022PhysicsHullFromRender\u0022 ).Count );\n\t\tAssert.AreEqual( 1, doc.FindObjects( \u0022RenderMeshList\u0022 ).Count );\n\t\tAssert.AreEqual( 1, doc.FindObjects( \u0022PhysicsShapeList\u0022 ).Count );\n\n\t\tvar mesh = doc.FindObjects( \u0022RenderMeshFile\u0022 ).Single();\n\t\tAssert.AreEqual( \u0022models/foo.fbx\u0022, ((Kv3Scalar)mesh.FindField( \u0022filename\u0022 ).Value).Value );\n\t\tAssert.AreEqual( \u00220.3937\u0022, ((Kv3Scalar)mesh.FindField( \u0022import_scale\u0022 ).Value).Value );\n\t\tAssert.AreEqual( \u0022BoundsCenter\u0022, ((Kv3Scalar)mesh.FindField( \u0022align_origin_x_type\u0022 ).Value).Value );\n\t\tAssert.AreEqual( \u0022BoundsMin\u0022, ((Kv3Scalar)mesh.FindField( \u0022align_origin_z_type\u0022 ).Value).Value );\n\n\t\t// Collision must carry the same align origin.\n\t\tvar hull = doc.FindObjects( \u0022PhysicsHullFromRender\u0022 ).Single();\n\t\tAssert.AreEqual( \u0022BoundsCenter\u0022, ((Kv3Scalar)hull.FindField( \u0022align_origin_x_type\u0022 ).Value).Value );\n\t\tAssert.AreEqual( \u0022BoundsCenter\u0022, ((Kv3Scalar)hull.FindField( \u0022align_origin_y_type\u0022 ).Value).Value );\n\t\tAssert.AreEqual( \u0022BoundsMin\u0022, ((Kv3Scalar)hull.FindField( \u0022align_origin_z_type\u0022 ).Value).Value );\n\t}\n\n\t[TestMethod]\n\tpublic void GeneratesMeshCollision()\n\t{\n\t\tvar options = new MeshToModelOptions\n\t\t{\n\t\t\tCollision = CollisionMode.Mesh,\n\t\t\tImportScale = 1.0f\n\t\t};\n\n\t\tvar source = VmdlGenerator.Generate( \u0022models/foo.fbx\u0022, options );\n\t\tvar doc = Kv3Document.Parse( source );\n\t\tAssert.AreEqual( 1, doc.FindObjects( \u0022PhysicsMeshFromRender\u0022 ).Count );\n\t\tAssert.AreEqual( 0, doc.FindObjects( \u0022PhysicsHullFromRender\u0022 ).Count );\n\t}\n\n\t[TestMethod]\n\tpublic void GeneratesFileCollision()\n\t{\n\t\tvar options = new MeshToModelOptions\n\t\t{\n\t\t\tCollision = CollisionMode.File,\n\t\t\tImportScale = 0.3937f\n\t\t};\n\n\t\tvar source = VmdlGenerator.Generate( \u0022models/foo.fbx\u0022, options );\n\t\tvar doc = Kv3Document.Parse( source );\n\t\tAssert.AreEqual( 1, doc.FindObjects( \u0022PhysicsHullFile\u0022 ).Count );\n\n\t\tvar hull = doc.FindObjects( \u0022PhysicsHullFile\u0022 ).Single();\n\t\tAssert.AreEqual( \u0022models/foo.fbx\u0022, ((Kv3Scalar)hull.FindField( \u0022filename\u0022 ).Value).Value );\n\t\tAssert.AreEqual( \u00220.3937\u0022, ((Kv3Scalar)hull.FindField( \u0022import_scale\u0022 ).Value).Value );\n\t}\n\n\t[TestMethod]\n\tpublic void NoCollisionWhenNone()\n\t{\n\t\tvar options = new MeshToModelOptions\n\t\t{\n\t\t\tCollision = CollisionMode.None,\n\t\t\tImportScale = 1.0f\n\t\t};\n\n\t\tvar source = VmdlGenerator.Generate( \u0022models/foo.fbx\u0022, options );\n\t\tvar doc = Kv3Document.Parse( source );\n\t\tAssert.AreEqual( 0, doc.FindObjects( \u0022PhysicsShapeList\u0022 ).Count );\n\t\tAssert.AreEqual( 1, doc.FindObjects( \u0022RenderMeshFile\u0022 ).Count );\n\t}\n\n\t[TestMethod]\n\tpublic void GeneratedVmdlIsEditableByBulkEditor()\n\t{\n\t\tvar options = new MeshToModelOptions\n\t\t{\n\t\t\tCollision = CollisionMode.Hull,\n\t\t\tImportScale = 0.3937f,\n\t\t\tAlignOriginX = AlignOrigin.BoundsCenter,\n\t\t\tAlignOriginY = AlignOrigin.BoundsCenter,\n\t\t\tAlignOriginZ = AlignOrigin.BoundsMin\n\t\t};\n\n\t\tvar source = VmdlGenerator.Generate( \u0022models/foo.fbx\u0022, options );\n\n\t\t// The generated file should load in the bulk editor and find its mesh entry.\n\t\tvar editor = VmdlBulkEditor.Load( source );\n\t\tAssert.AreEqual( 1, editor.MeshEntryCount );\n\n\t\t// And a further bulk edit should work on it.\n\t\tvar props = new MeshEntryProperties { ImportScale = 2.0f };\n\t\tAssert.IsTrue( editor.Apply( props ) );\n\n\t\tvar parsed = Kv3Document.Parse( editor.Source );\n\t\tvar mesh = parsed.FindObjects( \u0022RenderMeshFile\u0022 ).Single();\n\t\tAssert.AreEqual( \u00222.0\u0022, ((Kv3Scalar)mesh.FindField( \u0022import_scale\u0022 ).Value).Value );\n\t}\n}\n"},{"Ident":"bluedock.modelpro","Path":"UnitTests/LibraryTest.cs","FileName":"LibraryTest.cs","PackageType":"library","CodeKind":"UnitTest","AssetVersionId":340317,"Code":"using Sandbox;\r\n\r\n[TestClass]\r\npublic partial class LibraryTests\r\n{\r\n\t[TestMethod]\r\n\tpublic void SceneTest()\r\n\t{\r\n\t\tvar scene = new Scene();\r\n\t\tusing ( scene.Push() )\r\n\t\t{\r\n\t\t\tvar go = new GameObject();\r\n\r\n\t\t\tAssert.AreEqual( 1, scene.Directory.GameObjectCount );\r\n\t\t}\r\n\t}\r\n\r\n}\r\n"},{"Ident":"bluedock.modelpro","Path":"Editor/ModelProApp.cs","FileName":"ModelProApp.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340317,"Code":"using System;\n\nnamespace ModelPro.Editor;\n\n/// \u003Csummary\u003E\n/// Model Pro - an editor app for bulk editing model (.vmdl) properties and\n/// converting mesh files (.fbx) into models. Two tabs: VMDL bulk editing and\n/// FBX to model conversion.\n/// \u003C/summary\u003E\n[EditorApp( \u0022Model Pro\u0022, \u0022view_in_ar\u0022, \u0022Bulk edit model properties and convert meshes to models\u0022 )]\npublic class ModelProApp : Window\n{\n\tpublic ModelProApp()\n\t{\n\t\tWindowTitle = \u0022Model Pro\u0022;\n\t\tMinimumSize = new Vector2( 800, 500 );\n\t\tSetWindowIcon( \u0022view_in_ar\u0022 );\n\n\t\tBuildUI();\n\n\t\tShow();\n\t\tStateCookie = \u0022ModelPro\u0022;\n\t}\n\n\tprivate void BuildUI()\n\t{\n\t\tCanvas = new Widget( null );\n\t\tCanvas.Layout = Layout.Column();\n\t\tCanvas.Layout.Margin = 4;\n\n\t\tvar tabs = new TabWidget( Canvas );\n\t\ttabs.StateCookie = \u0022ModelProTabs\u0022;\n\t\tCanvas.Layout.Add( tabs, 1 );\n\n\t\ttabs.AddPage( \u0022DMX/FBX/OBJ\u0022, \u0022transform\u0022, new MeshConvertTab( tabs ) );\n\t\ttabs.AddPage( \u0022VMDL\u0022, \u0022view_in_ar\u0022, new VmdlEditTab( tabs ) );\n\t}\n\n\t[Menu( \u0022Editor\u0022, \u0022Model Pro/Open Model Pro\u0022 )]\n\tpublic static void OpenModelPro()\n\t{\n\t\tvar existing = Window.All.OfType\u003CModelProApp\u003E().FirstOrDefault();\n\t\tif ( existing.IsValid() )\n\t\t{\n\t\t\texisting.Show();\n\t\t\texisting.Focus();\n\t\t\treturn;\n\t\t}\n\n\t\tnew ModelProApp();\n\t}\n}\n"},{"Ident":"bluedock.modelpro","Path":"Editor/ModelProSettings.cs","FileName":"ModelProSettings.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340317,"Code":"using ModelPro.Vmdl;\n\nnamespace ModelPro.Editor;\n\n/// \u003Csummary\u003E\n/// Remembers the last values used in Model Pro so the next time the app is\n/// opened the controls are pre-filled. Persisted to the editor config folder.\n/// \u003C/summary\u003E\npublic class ModelProSettings\n{\n\tconst string Path = \u0022modelpro/modelpro.json\u0022;\n\n\t// VMDL tab\n\tpublic string VmdlScale { get; set; } = \u00221.0\u0022;\n\tpublic ScaleUnit VmdlScaleUnit { get; set; } = ScaleUnit.Inches;\n\tpublic AlignOrigin VmdlAlignX { get; set; } = AlignOrigin.None;\n\tpublic AlignOrigin VmdlAlignY { get; set; } = AlignOrigin.None;\n\tpublic AlignOrigin VmdlAlignZ { get; set; } = AlignOrigin.None;\n\tpublic string VmdlDefaultMaterial { get; set; } = \u0022\u0022;\n\n\t// FBX tab\n\tpublic CollisionMode FbxCollision { get; set; } = CollisionMode.Hull;\n\tpublic string FbxScale { get; set; } = \u00221.0\u0022;\n\tpublic ScaleUnit FbxScaleUnit { get; set; } = ScaleUnit.Inches;\n\tpublic AlignOrigin FbxAlignX { get; set; } = AlignOrigin.None;\n\tpublic AlignOrigin FbxAlignY { get; set; } = AlignOrigin.None;\n\tpublic AlignOrigin FbxAlignZ { get; set; } = AlignOrigin.None;\n\n\tprivate static readonly ModelProSettings _current = new();\n\n\t/// \u003Csummary\u003EThe single shared instance, loaded from disk on first access.\u003C/summary\u003E\n\tpublic static ModelProSettings Current\n\t{\n\t\tget\n\t\t{\n\t\t\tif ( _loaded ) return _current;\n\t\t\t_loaded = true;\n\n\t\t\ttry\n\t\t\t{\n\t\t\t\tglobal::Editor.FileSystem.Config.CreateDirectory( \u0022modelpro\u0022 );\n\t\t\t\tvar loaded = global::Editor.FileSystem.Config.ReadJsonOrDefault( Path, _current );\n\t\t\t\tif ( loaded is not null )\n\t\t\t\t{\n\t\t\t\t\t// Keep the path the same - only copy values over.\n\t\t\t\t\t_current.VmdlScale = loaded.VmdlScale;\n\t\t\t\t\t_current.VmdlScaleUnit = loaded.VmdlScaleUnit;\n\t\t\t\t\t_current.VmdlAlignX = loaded.VmdlAlignX;\n\t\t\t\t\t_current.VmdlAlignY = loaded.VmdlAlignY;\n\t\t\t\t\t_current.VmdlAlignZ = loaded.VmdlAlignZ;\n\t\t\t\t\t_current.VmdlDefaultMaterial = loaded.VmdlDefaultMaterial;\n\n\t\t\t\t\t_current.FbxCollision = loaded.FbxCollision;\n\t\t\t\t\t_current.FbxScale = loaded.FbxScale;\n\t\t\t\t\t_current.FbxScaleUnit = loaded.FbxScaleUnit;\n\t\t\t\t\t_current.FbxAlignX = loaded.FbxAlignX;\n\t\t\t\t\t_current.FbxAlignY = loaded.FbxAlignY;\n\t\t\t\t\t_current.FbxAlignZ = loaded.FbxAlignZ;\n\t\t\t\t}\n\t\t\t}\n\t\t\tcatch ( System.Exception e )\n\t\t\t{\n\t\t\t\tLog.Warning( e, \u0022Model Pro: failed to load settings\u0022 );\n\t\t\t}\n\n\t\t\treturn _current;\n\t\t}\n\t}\n\n\tstatic bool _loaded;\n\n\t/// \u003Csummary\u003ESave the current settings to the editor config folder.\u003C/summary\u003E\n\tpublic void Save()\n\t{\n\t\ttry\n\t\t{\n\t\t\tglobal::Editor.FileSystem.Config.CreateDirectory( \u0022modelpro\u0022 );\n\t\t\tglobal::Editor.FileSystem.Config.WriteJson( Path, _current );\n\t\t}\n\t\tcatch ( System.Exception e )\n\t\t{\n\t\t\tLog.Warning( e, \u0022Model Pro: failed to save settings\u0022 );\n\t\t}\n\t}\n}\n"},{"Ident":"bluedock.modelpro","Path":"Editor/Vmdl/VmdlBulkEditor.cs","FileName":"VmdlBulkEditor.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340317,"Code":"using System;\nusing System.Globalization;\nusing System.IO;\nusing System.Text;\n\nnamespace ModelPro.Vmdl;\n\n/// \u003Csummary\u003E\n/// Align origin options for each axis of a RenderMeshFile entry.\n/// These match the values used in ModelDoc\u0027s align_origin_*_type fields.\n/// \u003C/summary\u003E\npublic enum AlignOrigin\n{\n\tNone,\n\tCenter,\n\tMins,\n\tMaxs,\n\tBoundsCenter,\n\tBoundsMin,\n\tBoundsMax\n}\n\npublic static class AlignOriginExtensions\n{\n\tpublic static string ToKv3Value( this AlignOrigin value )\n\t{\n\t\treturn value switch\n\t\t{\n\t\t\tAlignOrigin.Center =\u003E \u0022Center\u0022,\n\t\t\tAlignOrigin.Mins =\u003E \u0022Mins\u0022,\n\t\t\tAlignOrigin.Maxs =\u003E \u0022Maxs\u0022,\n\t\t\tAlignOrigin.BoundsCenter =\u003E \u0022BoundsCenter\u0022,\n\t\t\tAlignOrigin.BoundsMin =\u003E \u0022BoundsMin\u0022,\n\t\t\tAlignOrigin.BoundsMax =\u003E \u0022BoundsMax\u0022,\n\t\t\t_ =\u003E \u0022None\u0022\n\t\t};\n\t}\n}\n\n/// \u003Csummary\u003E\n/// Collision modes for converting a mesh file into a model, matching the\n/// options in the asset browser\u0027s create-model popup.\n/// \u003C/summary\u003E\npublic enum CollisionMode\n{\n\t/// \u003Csummary\u003EA convex hull generated from the render geometry (PhysicsHullFromRender).\u003C/summary\u003E\n\tHull,\n\n\t/// \u003Csummary\u003EAn exact triangle mesh from the render geometry (PhysicsMeshFromRender).\u003C/summary\u003E\n\tMesh,\n\n\t/// \u003Csummary\u003EA hull file that references the source mesh file, with the import scale baked in (PhysicsHullFile).\u003C/summary\u003E\n\tFile,\n\n\t/// \u003Csummary\u003ENo collision.\u003C/summary\u003E\n\tNone\n}\n\npublic static class CollisionModeExtensions\n{\n\tpublic static string ToDisplayString( this CollisionMode mode )\n\t{\n\t\treturn mode switch\n\t\t{\n\t\t\tCollisionMode.Hull =\u003E \u0022Convex Hull\u0022,\n\t\t\tCollisionMode.Mesh =\u003E \u0022Exact Mesh\u0022,\n\t\t\tCollisionMode.File =\u003E \u0022File (Hull from FBX)\u0022,\n\t\t\t_ =\u003E \u0022None\u0022\n\t\t};\n\t}\n}\n\n/// \u003Csummary\u003E\n/// The unit the scale value is specified in, matching the units dropdown in the\n/// asset browser\u0027s create-model popup. Import scale stored in the vmdl is always\n/// in inches, so the entered value is multiplied by the conversion factor.\n/// \u003C/summary\u003E\npublic enum ScaleUnit\n{\n\tInches,\n\tFeet,\n\tMeters,\n\tCentimeters,\n\tMillimeters,\n\tCustom\n}\n\npublic static class ScaleUnitExtensions\n{\n\t/// \u003Csummary\u003EHow many inches one of these units is. Custom returns 1 - the value is used as-is.\u003C/summary\u003E\n\tpublic static float ToInches( this ScaleUnit unit )\n\t{\n\t\treturn unit switch\n\t\t{\n\t\t\tScaleUnit.Feet =\u003E 12.0f,\n\t\t\tScaleUnit.Meters =\u003E 39.3701f,\n\t\t\tScaleUnit.Centimeters =\u003E 0.3937f,\n\t\t\tScaleUnit.Millimeters =\u003E 0.03937f,\n\t\t\t_ =\u003E 1.0f\n\t\t};\n\t}\n\n\tpublic static string ToDisplayString( this ScaleUnit unit )\n\t{\n\t\treturn unit switch\n\t\t{\n\t\t\tScaleUnit.Feet =\u003E \u0022Feet (ft)\u0022,\n\t\t\tScaleUnit.Meters =\u003E \u0022Meters (m)\u0022,\n\t\t\tScaleUnit.Centimeters =\u003E \u0022Centimeters (cm)\u0022,\n\t\t\tScaleUnit.Millimeters =\u003E \u0022Millimeters (mm)\u0022,\n\t\t\tScaleUnit.Custom =\u003E \u0022Custom\u0022,\n\t\t\t_ =\u003E \u0022Inches (in)\u0022\n\t\t};\n\t}\n}\n\n/// \u003Csummary\u003E\n/// The properties that can be bulk-edited on every RenderMeshFile entry\n/// in a model\u0027s RenderMeshList, plus the model\u0027s default material group.\n/// \u003C/summary\u003E\npublic readonly struct MeshEntryProperties\n{\n\t/// \u003Csummary\u003EThe final import scale in inches, already converted from the chosen unit.\u003C/summary\u003E\n\tpublic float? ImportScale { get; init; }\n\tpublic AlignOrigin? AlignOriginX { get; init; }\n\tpublic AlignOrigin? AlignOriginY { get; init; }\n\tpublic AlignOrigin? AlignOriginZ { get; init; }\n\n\t/// \u003Csummary\u003E\n\t/// The global default material for the model\u0027s DefaultMaterialGroup\n\t/// (e.g. \u0022materials/default.vmat\u0022). Null leaves it unchanged.\n\t/// \u003C/summary\u003E\n\tpublic string GlobalDefaultMaterial { get; init; }\n}\n\n/// \u003Csummary\u003EA scale typed in a chosen unit, plus the unit it\u0027s in.\u003C/summary\u003E\npublic readonly struct ScaleInput\n{\n\tpublic float Value { get; init; }\n\tpublic ScaleUnit Unit { get; init; }\n\n\t/// \u003Csummary\u003E\n\t/// The value is the import_scale multiplier directly - what gets written to the\n\t/// vmdl. The unit is a display helper: it rescales the number when you switch\n\t/// units (1 inch shows as 0.3937 in cm, since 1 cm = 0.3937 inches).\n\t/// \u003C/summary\u003E\n\tpublic float ToImportScale() =\u003E Value;\n\n\t/// \u003Csummary\u003E\n\t/// Re-express this scale in another unit so the import scale stays the same.\n\t/// E.g. 1 inch converts to 0.3937 centimeters (1 cm = 0.3937 inches).\n\t/// \u003C/summary\u003E\n\tpublic ScaleInput ConvertTo( ScaleUnit newUnit )\n\t{\n\t\tif ( newUnit == Unit )\n\t\t\treturn this;\n\n\t\tvar converted = Value * newUnit.ToInches() / Unit.ToInches();\n\t\treturn new ScaleInput { Value = converted, Unit = newUnit };\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Convert a raw import scale into a display value for a given unit.\n\t/// \u003C/summary\u003E\n\tpublic static ScaleInput FromImportScale( float importScale, ScaleUnit unit )\n\t{\n\t\treturn new ScaleInput { Value = importScale, Unit = unit };\n\t}\n}\n\n/// \u003Csummary\u003E\n/// Loads a .vmdl source file, finds all RenderMeshFile mesh entries and applies\n/// bulk property edits to them, writing the result back while preserving the\n/// original file formatting.\n/// \u003C/summary\u003E\npublic sealed class VmdlBulkEditor\n{\n\treadonly Kv3Document _document;\n\treadonly List\u003CKv3Object\u003E _meshEntries;\n\tstring _source;\n\n\tpublic string Source =\u003E _source;\n\n\tpublic int MeshEntryCount =\u003E _meshEntries.Count;\n\n\tpublic bool HasRenderMeshList { get; }\n\n\tprivate VmdlBulkEditor( string source )\n\t{\n\t\t_source = source;\n\t\t_document = Kv3Document.Parse( source );\n\n\t\tHasRenderMeshList = _document.FindObjects( \u0022RenderMeshList\u0022 ).Count \u003E 0;\n\t\t_meshEntries = _document.FindObjects( \u0022RenderMeshFile\u0022 );\n\t}\n\n\tpublic static VmdlBulkEditor Load( string source ) =\u003E new( source );\n\n\tpublic static VmdlBulkEditor LoadFile( string path )\n\t{\n\t\treturn new VmdlBulkEditor( File.ReadAllText( path ) );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Apply the given properties to every mesh entry. Only properties that are\n\t/// non-null are changed. Returns true if anything was actually modified.\n\t/// \u003C/summary\u003E\n\tpublic bool Apply( MeshEntryProperties props )\n\t{\n\t\tvar edits = new List\u003C(int Start, int End, string Text)\u003E();\n\n\t\tforeach ( var entry in _meshEntries )\n\t\t{\n\t\t\tif ( props.ImportScale.HasValue )\n\t\t\t{\n\t\t\t\tSetField( entry, \u0022import_scale\u0022, FormatNumber( props.ImportScale.Value ), edits );\n\t\t\t}\n\n\t\t\tif ( props.AlignOriginX.HasValue )\n\t\t\t\tSetField( entry, \u0022align_origin_x_type\u0022, Quote( props.AlignOriginX.Value.ToKv3Value() ), edits );\n\n\t\t\tif ( props.AlignOriginY.HasValue )\n\t\t\t\tSetField( entry, \u0022align_origin_y_type\u0022, Quote( props.AlignOriginY.Value.ToKv3Value() ), edits );\n\n\t\t\tif ( props.AlignOriginZ.HasValue )\n\t\t\t\tSetField( entry, \u0022align_origin_z_type\u0022, Quote( props.AlignOriginZ.Value.ToKv3Value() ), edits );\n\t\t}\n\n\t\t// Align origin also needs to be set on the collision shapes so they stay\n\t\t// aligned with the render geometry - the physics nodes share the same\n\t\t// align_origin_*_type fields.\n\t\tif ( props.AlignOriginX.HasValue || props.AlignOriginY.HasValue || props.AlignOriginZ.HasValue )\n\t\t{\n\t\t\tforeach ( var shape in _document.FindObjects( \u0022PhysicsHullFromRender\u0022 ) )\n\t\t\t\tApplyAlignOrigin( shape, props, edits );\n\t\t\tforeach ( var shape in _document.FindObjects( \u0022PhysicsMeshFromRender\u0022 ) )\n\t\t\t\tApplyAlignOrigin( shape, props, edits );\n\t\t\tforeach ( var shape in _document.FindObjects( \u0022PhysicsHullFile\u0022 ) )\n\t\t\t\tApplyAlignOrigin( shape, props, edits );\n\t\t\tforeach ( var shape in _document.FindObjects( \u0022PhysicsMeshFile\u0022 ) )\n\t\t\t\tApplyAlignOrigin( shape, props, edits );\n\t\t}\n\n\t\tif ( props.GlobalDefaultMaterial is not null )\n\t\t{\n\t\t\tforeach ( var group in _document.FindObjects( \u0022DefaultMaterialGroup\u0022 ) )\n\t\t\t{\n\t\t\t\tSetField( group, \u0022global_default_material\u0022, Quote( props.GlobalDefaultMaterial ), edits );\n\t\t\t\tSetField( group, \u0022use_global_default\u0022, \u0022true\u0022, edits );\n\t\t\t}\n\t\t}\n\n\t\tif ( edits.Count == 0 )\n\t\t\treturn false;\n\n\t\tvar sb = new StringBuilder( _source );\n\t\tforeach ( var e in edits.OrderByDescending( x =\u003E x.Start ) )\n\t\t{\n\t\t\tsb.Remove( e.Start, e.End - e.Start );\n\t\t\tsb.Insert( e.Start, e.Text );\n\t\t}\n\n\t\t_source = sb.ToString();\n\t\treturn true;\n\t}\n\n\tprivate void ApplyAlignOrigin( Kv3Object shape, MeshEntryProperties props, List\u003C(int Start, int End, string Text)\u003E edits )\n\t{\n\t\tif ( props.AlignOriginX.HasValue )\n\t\t\tSetField( shape, \u0022align_origin_x_type\u0022, Quote( props.AlignOriginX.Value.ToKv3Value() ), edits );\n\n\t\tif ( props.AlignOriginY.HasValue )\n\t\t\tSetField( shape, \u0022align_origin_y_type\u0022, Quote( props.AlignOriginY.Value.ToKv3Value() ), edits );\n\n\t\tif ( props.AlignOriginZ.HasValue )\n\t\t\tSetField( shape, \u0022align_origin_z_type\u0022, Quote( props.AlignOriginZ.Value.ToKv3Value() ), edits );\n\t}\n\n\t/// \u003Csummary\u003EReturns the current state of a mesh entry\u0027s editable properties (from the first entry).\u003C/summary\u003E\n\tpublic MeshEntryProperties ReadFirstEntryProperties()\n\t{\n\t\tvar entry = _meshEntries.FirstOrDefault();\n\t\tif ( entry is null )\n\t\t\treturn default;\n\n\t\treturn new MeshEntryProperties\n\t\t{\n\t\t\tImportScale = ReadFloat( entry, \u0022import_scale\u0022 ),\n\t\t\tAlignOriginX = ReadAlign( entry, \u0022align_origin_x_type\u0022 ),\n\t\t\tAlignOriginY = ReadAlign( entry, \u0022align_origin_y_type\u0022 ),\n\t\t\tAlignOriginZ = ReadAlign( entry, \u0022align_origin_z_type\u0022 ),\n\t\t\tGlobalDefaultMaterial = ReadDefaultMaterial()\n\t\t};\n\t}\n\n\t/// \u003Csummary\u003EThe global default material path of the first DefaultMaterialGroup, or null.\u003C/summary\u003E\n\tpublic string ReadDefaultMaterial()\n\t{\n\t\tvar group = _document.FindObjects( \u0022DefaultMaterialGroup\u0022 ).FirstOrDefault();\n\t\tif ( group is null )\n\t\t\treturn null;\n\n\t\treturn (group.FindField( \u0022global_default_material\u0022 )?.Value as Kv3Scalar)?.Value;\n\t}\n\n\tprivate void SetField( Kv3Object obj, string key, string valueText, List\u003C(int Start, int End, string Text)\u003E edits )\n\t{\n\t\tvar field = obj.FindField( key );\n\n\t\tif ( field?.Value is Kv3Scalar scalar )\n\t\t{\n\t\t\tif ( scalar.Raw != valueText )\n\t\t\t\tedits.Add( (scalar.Start, scalar.End, valueText) );\n\t\t\treturn;\n\t\t}\n\n\t\t// Field doesn\u0027t exist - insert it right after the _class line.\n\t\tvar classField = obj.FindField( \u0022_class\u0022 );\n\t\tif ( classField?.Value is not Kv3Scalar classScalar )\n\t\t\treturn;\n\n\t\tint lineEnd = _source.IndexOf( \u0027\\n\u0027, classScalar.End );\n\t\tif ( lineEnd \u003C 0 )\n\t\t\tlineEnd = _source.Length;\n\n\t\tint lineStart = _source.LastIndexOf( \u0027\\n\u0027, Math.Max( 0, classField.KeyStart - 1 ) ) \u002B 1;\n\t\tvar indent = _source.Substring( lineStart, classField.KeyStart - lineStart );\n\n\t\tstring insert = \u0022\\n\u0022 \u002B indent \u002B key \u002B \u0022 = \u0022 \u002B valueText;\n\t\tedits.Add( (lineEnd, lineEnd, insert) );\n\t}\n\n\tprivate static float? ReadFloat( Kv3Object obj, string key )\n\t{\n\t\tif ( obj.FindField( key )?.Value is Kv3Scalar s \u0026\u0026\n\t\t\tfloat.TryParse( s.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var value ) )\n\t\t{\n\t\t\treturn value;\n\t\t}\n\n\t\treturn null;\n\t}\n\n\tprivate static AlignOrigin? ReadAlign( Kv3Object obj, string key )\n\t{\n\t\tif ( obj.FindField( key )?.Value is Kv3Scalar s )\n\t\t{\n\t\t\treturn s.Value switch\n\t\t\t{\n\t\t\t\t\u0022Center\u0022 =\u003E AlignOrigin.Center,\n\t\t\t\t\u0022Mins\u0022 =\u003E AlignOrigin.Mins,\n\t\t\t\t\u0022Maxs\u0022 =\u003E AlignOrigin.Maxs,\n\t\t\t\t\u0022BoundsCenter\u0022 =\u003E AlignOrigin.BoundsCenter,\n\t\t\t\t\u0022BoundsMin\u0022 =\u003E AlignOrigin.BoundsMin,\n\t\t\t\t\u0022BoundsMax\u0022 =\u003E AlignOrigin.BoundsMax,\n\t\t\t\t_ =\u003E AlignOrigin.None\n\t\t\t};\n\t\t}\n\n\t\treturn null;\n\t}\n\n\tprivate static string FormatNumber( float value )\n\t{\n\t\treturn value.ToString( \u00220.0########\u0022, CultureInfo.InvariantCulture );\n\t}\n\n\tprivate static string Quote( string value ) =\u003E $\u0022\\\u0022{value}\\\u0022\u0022;\n}\n"},{"Ident":"bluedock.modelpro","Path":"Editor/Assembly.cs","FileName":"Assembly.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340317,"Code":"global using Sandbox;\nglobal using Editor;\nglobal using System.Collections.Generic;\nglobal using System.Linq;\n"},{"Ident":"bluedock.modelpro","Path":"Editor/MeshConvertTab.cs","FileName":"MeshConvertTab.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340317,"Code":"using System;\nusing System.Globalization;\nusing System.IO;\nusing ModelPro.Vmdl;\n\nnamespace ModelPro.Editor;\n\n/// \u003Csummary\u003E\n/// The DMX/FBX/OBJ tab of Model Pro. Pick a folder, it recursively finds every\n/// mesh file (.fbx, .obj, .dmx) and converts them all into .vmdl models with the\n/// chosen collision, scale and align-origin settings.\n/// \u003C/summary\u003E\npublic class MeshConvertTab : Widget\n{\n\tTreeView _folderTree;\n\tTreeView _fileList;\n\tLabel _statusLabel;\n\n\tComboBox _collision;\n\tLineEdit _scaleEdit;\n\tComboBox _scaleUnit;\n\tScaleUnit _lastScaleUnit = ScaleUnit.Inches;\n\tComboBox _alignX;\n\tComboBox _alignY;\n\tComboBox _alignZ;\n\tButton _convertButton;\n\tLabel _resultLabel;\n\n\tList\u003Cstring\u003E _foundMeshFiles = new();\n\n\tpublic MeshConvertTab( Widget parent ) : base( parent )\n\t{\n\t\tBuildUI();\n\t\tLoadSettings();\n\t}\n\n\tprivate void BuildUI()\n\t{\n\t\tLayout = Layout.Column();\n\t\tLayout.Spacing = 4;\n\n\t\tvar split = Layout.AddRow();\n\t\tsplit.Spacing = 8;\n\n\t\tvar left = split.AddColumn();\n\t\tleft.Spacing = 4;\n\t\tleft.Add( new Label( \u0022\u003Cb\u003EFolder\u003C/b\u003E\u0022 ) );\n\n\t\tvar treeScroll = left.Add( new ScrollArea( this ), 1 );\n\t\ttreeScroll.Canvas = new Widget();\n\t\ttreeScroll.Canvas.Layout = Layout.Column();\n\t\ttreeScroll.Canvas.Layout.Margin = new Sandbox.UI.Margin( 2 );\n\n\t\t_folderTree = new TreeView( treeScroll.Canvas );\n\t\ttreeScroll.Canvas.Layout.Add( _folderTree );\n\t\t_folderTree.ExpandForSelection = true;\n\n\t\tvar rootDir = Sandbox.Project.Current?.RootDirectory;\n\t\tif ( rootDir is not null )\n\t\t{\n\t\t\tvar root = new FolderNode( rootDir.FullName, OnFolderSelected );\n\t\t\t_folderTree.AddItem( root );\n\t\t\t_folderTree.Open( root );\n\t\t}\n\n\t\tvar right = split.AddColumn();\n\t\tright.Spacing = 4;\n\n\t\t_statusLabel = right.Add( new Label( \u0022Select a folder to search for mesh files (.fbx, .obj, .dmx).\u0022 ) );\n\t\t_statusLabel.WordWrap = true;\n\n\t\tvar listScroll = right.Add( new ScrollArea( this ), 1 );\n\t\tlistScroll.Canvas = new Widget();\n\t\tlistScroll.Canvas.Layout = Layout.Column();\n\t\tlistScroll.Canvas.Layout.Margin = new Sandbox.UI.Margin( 2 );\n\n\t\t_fileList = new TreeView( listScroll.Canvas );\n\t\tlistScroll.Canvas.Layout.Add( _fileList );\n\n\t\tvar group = right.Add( new Widget() );\n\t\tvar grid = Layout.Grid();\n\t\tgrid.Spacing = 4;\n\t\tgroup.Layout = grid;\n\n\t\tint row = 0;\n\n\t\tgrid.AddCell( 0, row, new Label( \u0022Collision\u0022 ) );\n\t\t_collision = new ComboBox() { FixedWidth = 150 };\n\t\tforeach ( var mode in Enum.GetValues\u003CCollisionMode\u003E() )\n\t\t\t_collision.AddItem( mode.ToDisplayString() );\n\t\t_collision.TrySelectNamed( CollisionMode.Hull.ToDisplayString() );\n\t\tgrid.AddCell( 1, row, _collision );\n\t\trow\u002B\u002B;\n\n\t\tgrid.AddCell( 0, row, new Label( \u0022Scale\u0022 ) );\n\n\t\tvar scaleRow = Layout.Row();\n\t\tscaleRow.Spacing = 4;\n\t\t_scaleEdit = new LineEdit() { Text = \u00221.0\u0022, FixedWidth = 90 };\n\t\t_scaleUnit = new ComboBox() { FixedWidth = 130 };\n\t\tforeach ( var unit in Enum.GetValues\u003CScaleUnit\u003E() )\n\t\t{\n\t\t\tvar u = unit;\n\t\t\t_scaleUnit.AddItem( u.ToDisplayString(), onSelected: () =\u003E OnScaleUnitChanged( u ) );\n\t\t}\n\t\t_scaleUnit.TrySelectNamed( ScaleUnit.Inches.ToDisplayString() );\n\t\tscaleRow.Add( _scaleEdit );\n\t\tscaleRow.Add( _scaleUnit );\n\t\tgrid.AddCell( 1, row, scaleRow );\n\t\trow\u002B\u002B;\n\n\t\tgrid.AddCell( 0, row, new Label( \u0022Align Origin X\u0022 ) );\n\t\t_alignX = CreateAlignCombo();\n\t\tgrid.AddCell( 1, row, _alignX );\n\t\trow\u002B\u002B;\n\n\t\tgrid.AddCell( 0, row, new Label( \u0022Align Origin Y\u0022 ) );\n\t\t_alignY = CreateAlignCombo();\n\t\tgrid.AddCell( 1, row, _alignY );\n\t\trow\u002B\u002B;\n\n\t\tgrid.AddCell( 0, row, new Label( \u0022Align Origin Z\u0022 ) );\n\t\t_alignZ = CreateAlignCombo();\n\t\tgrid.AddCell( 1, row, _alignZ );\n\t\trow\u002B\u002B;\n\n\t\t_convertButton = new Button( \u0022Convert to N models\u0022, \u0022transform\u0022 );\n\t\t_convertButton.Clicked \u002B= ConvertAll;\n\t\tgrid.AddCell( 0, row, _convertButton );\n\t\trow\u002B\u002B;\n\n\t\t_resultLabel = new Label( \u0022\u0022 );\n\t\t_resultLabel.WordWrap = true;\n\t\tright.Add( _resultLabel );\n\t}\n\n\tprivate ComboBox CreateAlignCombo()\n\t{\n\t\tvar combo = new ComboBox() { FixedWidth = 120 };\n\t\tcombo.AddItem( \u0022None\u0022 );\n\t\tcombo.AddItem( \u0022BoundsCenter\u0022 );\n\t\tcombo.AddItem( \u0022BoundsMin\u0022 );\n\t\tcombo.AddItem( \u0022BoundsMax\u0022 );\n\t\tcombo.TrySelectNamed( \u0022None\u0022 );\n\t\treturn combo;\n\t}\n\n\t/// \u003Csummary\u003ERestore the last used values into the controls.\u003C/summary\u003E\n\tprivate void LoadSettings()\n\t{\n\t\tvar s = ModelProSettings.Current;\n\n\t\t_collision.TrySelectNamed( s.FbxCollision.ToDisplayString() );\n\t\t_scaleEdit.Text = s.FbxScale;\n\t\t_lastScaleUnit = s.FbxScaleUnit;\n\t\t_scaleUnit.TrySelectNamed( s.FbxScaleUnit.ToDisplayString() );\n\t\t_alignX.TrySelectNamed( s.FbxAlignX.ToKv3Value() );\n\t\t_alignY.TrySelectNamed( s.FbxAlignY.ToKv3Value() );\n\t\t_alignZ.TrySelectNamed( s.FbxAlignZ.ToKv3Value() );\n\t}\n\n\t/// \u003Csummary\u003ERemember the current values for next time.\u003C/summary\u003E\n\tprivate void SaveSettings()\n\t{\n\t\tvar s = ModelProSettings.Current;\n\t\ts.FbxCollision = ParseCollision( _collision.CurrentText );\n\t\ts.FbxScale = _scaleEdit.Text;\n\t\ts.FbxScaleUnit = ParseScaleUnit( _scaleUnit.CurrentText );\n\t\ts.FbxAlignX = ParseAlign( _alignX.CurrentText );\n\t\ts.FbxAlignY = ParseAlign( _alignY.CurrentText );\n\t\ts.FbxAlignZ = ParseAlign( _alignZ.CurrentText );\n\t\ts.Save();\n\t}\n\n\tprivate void OnFolderSelected( string folder )\n\t{\n\t\t_statusLabel.Text = $\u0022Searching \u003Cb\u003E{folder}\u003C/b\u003E...\u0022;\n\t\t_resultLabel.Text = \u0022\u0022;\n\n\t\t_foundMeshFiles = Directory.GetFiles( folder, \u0022*.fbx\u0022, SearchOption.AllDirectories )\n\t\t\t.Concat( Directory.GetFiles( folder, \u0022*.obj\u0022, SearchOption.AllDirectories ) )\n\t\t\t.Concat( Directory.GetFiles( folder, \u0022*.dmx\u0022, SearchOption.AllDirectories ) )\n\t\t\t.Distinct( StringComparer.OrdinalIgnoreCase )\n\t\t\t.OrderBy( x =\u003E x, StringComparer.OrdinalIgnoreCase )\n\t\t\t.ToList();\n\n\t\t_fileList.Clear();\n\t\tforeach ( var file in _foundMeshFiles )\n\t\t\t_fileList.AddItem( new FileNode( file, 0 ) );\n\n\t\t_statusLabel.Text = $\u0022Found \u003Cb\u003E{_foundMeshFiles.Count}\u003C/b\u003E mesh files in \u003Cb\u003E{folder}\u003C/b\u003E.\u0022;\n\n\t\t_convertButton.Text = _foundMeshFiles.Count == 0\n\t\t\t? \u0022Convert to 0 models\u0022\n\t\t\t: $\u0022Convert to {_foundMeshFiles.Count} models\u0022;\n\t}\n\n\tprivate void ConvertAll()\n\t{\n\t\tif ( _foundMeshFiles.Count == 0 )\n\t\t\treturn;\n\n\t\tvar options = new MeshToModelOptions\n\t\t{\n\t\t\tCollision = ParseCollision( _collision.CurrentText ),\n\t\t\tImportScale = ReadScale() ?? 1.0f,\n\t\t\tAlignOriginX = ParseAlign( _alignX.CurrentText ),\n\t\t\tAlignOriginY = ParseAlign( _alignY.CurrentText ),\n\t\t\tAlignOriginZ = ParseAlign( _alignZ.CurrentText )\n\t\t};\n\n\t\tvar assetsRoot = Sandbox.Project.Current?.GetAssetsPath();\n\t\tif ( string.IsNullOrEmpty( assetsRoot ) )\n\t\t\treturn;\n\n\t\tint created = 0;\n\t\tint skipped = 0;\n\t\tvar errors = new List\u003Cstring\u003E();\n\n\t\tforeach ( var mesh in _foundMeshFiles )\n\t\t{\n\t\t\ttry\n\t\t\t{\n\t\t\t\tvar vmdlPath = Path.ChangeExtension( mesh, \u0022.vmdl\u0022 );\n\n\t\t\t\t// Don\u0027t overwrite an existing model.\n\t\t\t\tif ( File.Exists( vmdlPath ) )\n\t\t\t\t{\n\t\t\t\t\tskipped\u002B\u002B;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tvar relative = mesh.Replace( \u0027\\\\\u0027, \u0027/\u0027 );\n\t\t\t\tvar assetsRootNorm = assetsRoot.Replace( \u0027\\\\\u0027, \u0027/\u0027 );\n\t\t\t\tif ( relative.StartsWith( assetsRootNorm, StringComparison.OrdinalIgnoreCase ) )\n\t\t\t\t\trelative = relative.Substring( assetsRootNorm.Length ).TrimStart( \u0027/\u0027 );\n\n\t\t\t\tvar source = VmdlGenerator.Generate( relative, options );\n\t\t\t\tFile.WriteAllText( vmdlPath, source );\n\n\t\t\t\tRegisterAndCompile( vmdlPath );\n\t\t\t\tcreated\u002B\u002B;\n\t\t\t}\n\t\t\tcatch ( Exception e )\n\t\t\t{\n\t\t\t\terrors.Add( $\u0022{Path.GetFileName( mesh )}: {e.Message}\u0022 );\n\t\t\t}\n\t\t}\n\n\t\tvar msg = $\u0022Created \u003Cb\u003E{created}\u003C/b\u003E models\u0022 \u002B\n\t\t\t(skipped \u003E 0 ? $\u0022 ({skipped} skipped \u2014 already exist)\u0022 : \u0022\u0022) \u002B \u0022.\u0022;\n\t\tif ( errors.Count \u003E 0 )\n\t\t\tmsg \u002B= $\u0022\\n\\nErrors ({errors.Count}):\\n{string.Join( \u0022\\n\u0022, errors.Take( 8 ) )}\u0022;\n\n\t\t_resultLabel.Text = msg;\n\n\t\tSaveSettings();\n\t}\n\n\tprivate static void RegisterAndCompile( string vmdlPath )\n\t{\n\t\tvar asset = AssetSystem.RegisterFile( vmdlPath );\n\t\tif ( asset is null )\n\t\t{\n\t\t\tLog.Warning( $\u0022Model Pro: failed to register {vmdlPath}\u0022 );\n\t\t\treturn;\n\t\t}\n\n\t\tasset.Compile( true );\n\t}\n\n\tprivate float? ReadScale()\n\t{\n\t\tif ( !float.TryParse( _scaleEdit.Text, NumberStyles.Float, CultureInfo.InvariantCulture, out var value ) )\n\t\t\treturn null;\n\n\t\tvar unit = ParseScaleUnit( _scaleUnit.CurrentText );\n\t\treturn new ScaleInput { Value = value, Unit = unit }.ToImportScale();\n\t}\n\n\tprivate void OnScaleUnitChanged( ScaleUnit newUnit )\n\t{\n\t\tif ( newUnit == _lastScaleUnit )\n\t\t\treturn;\n\n\t\tif ( float.TryParse( _scaleEdit.Text, NumberStyles.Float, CultureInfo.InvariantCulture, out var current ) )\n\t\t{\n\t\t\tvar converted = new ScaleInput { Value = current, Unit = _lastScaleUnit }.ConvertTo( newUnit );\n\t\t\t_scaleEdit.Text = converted.Value.ToString( \u00220.0########\u0022, CultureInfo.InvariantCulture );\n\t\t}\n\n\t\t_lastScaleUnit = newUnit;\n\t}\n\n\tprivate static ScaleUnit ParseScaleUnit( string text )\n\t{\n\t\tforeach ( var unit in Enum.GetValues\u003CScaleUnit\u003E() )\n\t\t{\n\t\t\tif ( string.Equals( unit.ToDisplayString(), text, StringComparison.OrdinalIgnoreCase ) )\n\t\t\t\treturn unit;\n\t\t}\n\n\t\treturn ScaleUnit.Inches;\n\t}\n\n\tprivate static CollisionMode ParseCollision( string text )\n\t{\n\t\tforeach ( var mode in Enum.GetValues\u003CCollisionMode\u003E() )\n\t\t{\n\t\t\tif ( string.Equals( mode.ToDisplayString(), text, StringComparison.OrdinalIgnoreCase ) )\n\t\t\t\treturn mode;\n\t\t}\n\n\t\treturn CollisionMode.Hull;\n\t}\n\n\tprivate static AlignOrigin ParseAlign( string text )\n\t{\n\t\treturn text switch\n\t\t{\n\t\t\t\u0022BoundsCenter\u0022 =\u003E AlignOrigin.BoundsCenter,\n\t\t\t\u0022BoundsMin\u0022 =\u003E AlignOrigin.BoundsMin,\n\t\t\t\u0022BoundsMax\u0022 =\u003E AlignOrigin.BoundsMax,\n\t\t\t\u0022Center\u0022 =\u003E AlignOrigin.Center,\n\t\t\t\u0022Mins\u0022 =\u003E AlignOrigin.Mins,\n\t\t\t\u0022Maxs\u0022 =\u003E AlignOrigin.Maxs,\n\t\t\t_ =\u003E AlignOrigin.None\n\t\t};\n\t}\n}\n"},{"Ident":"bluedock.modelpro","Path":"UnitTests/AlignOriginTests.cs","FileName":"AlignOriginTests.cs","PackageType":"library","CodeKind":"UnitTest","AssetVersionId":340317,"Code":"using System;\nusing System.Linq;\nusing Microsoft.VisualStudio.TestTools.UnitTesting;\nusing ModelPro.Vmdl;\n\n[TestClass]\npublic class AlignOriginTests\n{\n\tconst string ToolFile = \u0022\u0022\u0022\n\t{\n\t\trootNode = \n\t\t{\n\t\t\t_class = \u0022RootNode\u0022\n\t\t\tchildren = \n\t\t\t[\n\t\t\t\t{\n\t\t\t\t\t_class = \u0022RenderMeshList\u0022\n\t\t\t\t\tchildren = \n\t\t\t\t\t[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t_class = \u0022RenderMeshFile\u0022\n\t\t\t\t\t\t\tfilename = \u0022models/vehicle_01_a.fbx\u0022\n\t\t\t\t\t\t\timport_scale = 1.0\n\t\t\t\t\t\t\talign_origin_x_type = \u0022Center\u0022\n\t\t\t\t\t\t\talign_origin_y_type = \u0022Center\u0022\n\t\t\t\t\t\t\talign_origin_z_type = \u0022Mins\u0022\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t]\n\t\t}\n\t}\n\t\u0022\u0022\u0022;\n\n\t[TestMethod]\n\tpublic void AllAlignOriginsRoundTrip()\n\t{\n\t\tforeach ( var value in Enum.GetValues\u003CAlignOrigin\u003E() )\n\t\t{\n\t\t\tvar kv3 = value.ToKv3Value();\n\n\t\t\tvar editor = VmdlBulkEditor.Load( ToolFile );\n\t\t\tvar props = new MeshEntryProperties\n\t\t\t{\n\t\t\t\tAlignOriginX = value,\n\t\t\t\tAlignOriginY = value,\n\t\t\t\tAlignOriginZ = value\n\t\t\t};\n\n\t\t\tAssert.IsTrue( editor.Apply( props ), $\u0022{value} should change the file\u0022 );\n\n\t\t\tvar parsed = Kv3Document.Parse( editor.Source );\n\t\t\tvar entry = parsed.FindObjects( \u0022RenderMeshFile\u0022 ).Single();\n\t\t\tAssert.AreEqual( kv3, ((Kv3Scalar)entry.FindField( \u0022align_origin_x_type\u0022 ).Value).Value, value.ToString() );\n\t\t}\n\t}\n\n\t[TestMethod]\n\tpublic void BoundsValuesAreWritable()\n\t{\n\t\tvar editor = VmdlBulkEditor.Load( ToolFile );\n\t\tvar props = new MeshEntryProperties\n\t\t{\n\t\t\tImportScale = 0.3937f,\n\t\t\tAlignOriginX = AlignOrigin.BoundsCenter,\n\t\t\tAlignOriginY = AlignOrigin.BoundsCenter,\n\t\t\tAlignOriginZ = AlignOrigin.BoundsMin\n\t\t};\n\n\t\tAssert.IsTrue( editor.Apply( props ), \u0022Bounds* align values should apply\u0022 );\n\n\t\tvar parsed = Kv3Document.Parse( editor.Source );\n\t\tvar entry = parsed.FindObjects( \u0022RenderMeshFile\u0022 ).Single();\n\t\tAssert.AreEqual( \u00220.3937\u0022, ((Kv3Scalar)entry.FindField( \u0022import_scale\u0022 ).Value).Value );\n\t\tAssert.AreEqual( \u0022BoundsCenter\u0022, ((Kv3Scalar)entry.FindField( \u0022align_origin_x_type\u0022 ).Value).Value );\n\t\tAssert.AreEqual( \u0022BoundsCenter\u0022, ((Kv3Scalar)entry.FindField( \u0022align_origin_y_type\u0022 ).Value).Value );\n\t\tAssert.AreEqual( \u0022BoundsMin\u0022, ((Kv3Scalar)entry.FindField( \u0022align_origin_z_type\u0022 ).Value).Value );\n\t}\n}\n"},{"Ident":"bluedock.modelpro","Path":"UnitTests/UnitTest.cs","FileName":"UnitTest.cs","PackageType":"library","CodeKind":"UnitTest","AssetVersionId":340317,"Code":"global using Microsoft.VisualStudio.TestTools.UnitTesting;\r\n\r\n[TestClass]\r\npublic class TestInit\r\n{\r\n\tpublic static Sandbox.TestAppSystem AppSystem;\r\n\r\n\t[AssemblyInitialize]\r\n\tpublic static void AssemblyInitialize( TestContext context )\r\n\t{\r\n\t\tAppSystem = new Sandbox.TestAppSystem();\r\n\t\tAppSystem.Init();\r\n\t}\r\n\r\n\t[AssemblyCleanup]\r\n\tpublic static void AssemblyCleanup()\r\n\t{\r\n\t\tAppSystem.Shutdown();\r\n\t}\r\n}\r\n"},{"Ident":"bluedock.modelpro","Path":"Editor/Vmdl/VmdlGenerator.cs","FileName":"VmdlGenerator.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340317,"Code":"using System.Globalization;\nusing System.Text;\n\nnamespace ModelPro.Vmdl;\n\n/// \u003Csummary\u003E\n/// Options for converting a mesh file (.fbx, .obj, .dmx) into a model (.vmdl),\n/// matching the asset browser\u0027s create-model popup.\n/// \u003C/summary\u003E\npublic struct MeshToModelOptions\n{\n\t/// \u003Csummary\u003EThe collision shape to generate for the model.\u003C/summary\u003E\n\tpublic CollisionMode Collision { get; set; }\n\n\t/// \u003Csummary\u003EThe import scale (in inches) to apply to the mesh.\u003C/summary\u003E\n\tpublic float ImportScale { get; set; }\n\n\tpublic AlignOrigin AlignOriginX { get; set; }\n\tpublic AlignOrigin AlignOriginY { get; set; }\n\tpublic AlignOrigin AlignOriginZ { get; set; }\n}\n\n/// \u003Csummary\u003E\n/// Generates the KV3 text of a model (.vmdl) file from a source mesh file,\n/// writing the same structure ModelDoc produces so it compiles cleanly.\n/// \u003C/summary\u003E\npublic static class VmdlGenerator\n{\n\tconst string Header = \u0022\u003C!-- kv3 encoding:text:version{e21c7f3c-8a33-41c5-9977-a76d3a32aa0d} format:modeldoc30:version{8c2d7a91-9c42-4bf0-883a-5a3b1762d4f1} --\u003E\u0022;\n\n\t/// \u003Csummary\u003E\n\t/// Build the vmdl source text for a mesh file at the given content-relative\n\t/// path (e.g. \u0022models/foo.fbx\u0022).\n\t/// \u003C/summary\u003E\n\tpublic static string Generate( string meshRelativePath, MeshToModelOptions options )\n\t{\n\t\tvar sb = new StringBuilder();\n\n\t\tsb.AppendLine( Header );\n\t\tsb.AppendLine( \u0022{\u0022 );\n\t\tsb.AppendLine( \u0022\\trootNode = \u0022 );\n\t\tsb.AppendLine( \u0022\\t{\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t_class = \\\u0022RootNode\\\u0022\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\tchildren = \u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t[\u0022 );\n\n\t\t// Material group\n\t\tsb.AppendLine( \u0022\\t\\t\\t{\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t_class = \\\u0022MaterialGroupList\\\u0022\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\tchildren = \u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t[\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t{\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\t_class = \\\u0022DefaultMaterialGroup\\\u0022\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\tremaps = [  ]\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\tuse_global_default = true\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\tglobal_default_material = \\\u0022materials/default.vmat\\\u0022\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t},\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t]\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t},\u0022 );\n\n\t\t// Physics shape list\n\t\tAppendPhysicsShapeList( sb, meshRelativePath, options );\n\n\t\t// Render mesh list\n\t\tAppendRenderMeshList( sb, meshRelativePath, options );\n\n\t\tsb.AppendLine( \u0022\\t\\t]\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\tmodel_archetype = \\\u0022\\\u0022\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\tprimary_associated_entity = \\\u0022\\\u0022\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\tanim_graph_name = \\\u0022\\\u0022\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\tbase_model_name = \\\u0022\\\u0022\u0022 );\n\t\tsb.AppendLine( \u0022\\t}\u0022 );\n\t\tsb.AppendLine( \u0022}\u0022 );\n\n\t\treturn sb.ToString();\n\t}\n\n\tprivate static void AppendRenderMeshList( StringBuilder sb, string meshRelativePath, MeshToModelOptions options )\n\t{\n\t\tsb.AppendLine( \u0022\\t\\t\\t{\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t_class = \\\u0022RenderMeshList\\\u0022\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\tchildren = \u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t[\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t{\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\t_class = \\\u0022RenderMeshFile\\\u0022\u0022 );\n\t\tsb.AppendLine( $\u0022\\t\\t\\t\\t\\t\\tfilename = \\\u0022{meshRelativePath}\\\u0022\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\timport_translation = [ 0.0, 0.0, 0.0 ]\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\timport_rotation = [ 0.0, 0.0, 0.0 ]\u0022 );\n\t\tsb.AppendLine( $\u0022\\t\\t\\t\\t\\t\\timport_scale = {FormatNumber( options.ImportScale )}\u0022 );\n\t\tsb.AppendLine( $\u0022\\t\\t\\t\\t\\t\\talign_origin_x_type = \\\u0022{options.AlignOriginX.ToKv3Value()}\\\u0022\u0022 );\n\t\tsb.AppendLine( $\u0022\\t\\t\\t\\t\\t\\talign_origin_y_type = \\\u0022{options.AlignOriginY.ToKv3Value()}\\\u0022\u0022 );\n\t\tsb.AppendLine( $\u0022\\t\\t\\t\\t\\t\\talign_origin_z_type = \\\u0022{options.AlignOriginZ.ToKv3Value()}\\\u0022\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\tparent_bone = \\\u0022\\\u0022\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\timport_filter = \u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\t{\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\t\\texclude_by_default = false\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\t}\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t},\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t]\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t},\u0022 );\n\t}\n\n\tprivate static void AppendPhysicsShapeList( StringBuilder sb, string meshRelativePath, MeshToModelOptions options )\n\t{\n\t\tif ( options.Collision == CollisionMode.None )\n\t\t\treturn;\n\n\t\tsb.AppendLine( \u0022\\t\\t\\t{\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t_class = \\\u0022PhysicsShapeList\\\u0022\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\tchildren = \u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t[\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t{\u0022 );\n\n\t\tswitch ( options.Collision )\n\t\t{\n\t\t\tcase CollisionMode.Mesh:\n\t\t\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\t_class = \\\u0022PhysicsMeshFromRender\\\u0022\u0022 );\n\t\t\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\tparent_bone = \\\u0022\\\u0022\u0022 );\n\t\t\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\tsurface_prop = \\\u0022default\\\u0022\u0022 );\n\t\t\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\tcollision_tags = \\\u0022solid\\\u0022\u0022 );\n\t\t\t\tsb.AppendLine( $\u0022\\t\\t\\t\\t\\t\\talign_origin_x_type = \\\u0022{options.AlignOriginX.ToKv3Value()}\\\u0022\u0022 );\n\t\t\t\tsb.AppendLine( $\u0022\\t\\t\\t\\t\\t\\talign_origin_y_type = \\\u0022{options.AlignOriginY.ToKv3Value()}\\\u0022\u0022 );\n\t\t\t\tsb.AppendLine( $\u0022\\t\\t\\t\\t\\t\\talign_origin_z_type = \\\u0022{options.AlignOriginZ.ToKv3Value()}\\\u0022\u0022 );\n\t\t\t\tbreak;\n\n\t\t\tcase CollisionMode.File:\n\t\t\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\t_class = \\\u0022PhysicsHullFile\\\u0022\u0022 );\n\t\t\t\tsb.AppendLine( $\u0022\\t\\t\\t\\t\\t\\tfilename = \\\u0022{meshRelativePath}\\\u0022\u0022 );\n\t\t\t\tsb.AppendLine( $\u0022\\t\\t\\t\\t\\t\\timport_scale = {FormatNumber( options.ImportScale )}\u0022 );\n\t\t\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\tparent_bone = \\\u0022\\\u0022\u0022 );\n\t\t\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\tsurface_prop = \\\u0022default\\\u0022\u0022 );\n\t\t\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\tcollision_tags = \\\u0022solid\\\u0022\u0022 );\n\t\t\t\tsb.AppendLine( $\u0022\\t\\t\\t\\t\\t\\talign_origin_x_type = \\\u0022{options.AlignOriginX.ToKv3Value()}\\\u0022\u0022 );\n\t\t\t\tsb.AppendLine( $\u0022\\t\\t\\t\\t\\t\\talign_origin_y_type = \\\u0022{options.AlignOriginY.ToKv3Value()}\\\u0022\u0022 );\n\t\t\t\tsb.AppendLine( $\u0022\\t\\t\\t\\t\\t\\talign_origin_z_type = \\\u0022{options.AlignOriginZ.ToKv3Value()}\\\u0022\u0022 );\n\t\t\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\tfaceMergeAngle = 20.0\u0022 );\n\t\t\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\tmaxHullVertices = 32\u0022 );\n\t\t\t\tbreak;\n\n\t\t\tcase CollisionMode.Hull:\n\t\t\tdefault:\n\t\t\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\t_class = \\\u0022PhysicsHullFromRender\\\u0022\u0022 );\n\t\t\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\tparent_bone = \\\u0022\\\u0022\u0022 );\n\t\t\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\tsurface_prop = \\\u0022default\\\u0022\u0022 );\n\t\t\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\tcollision_tags = \\\u0022solid\\\u0022\u0022 );\n\t\t\t\tsb.AppendLine( $\u0022\\t\\t\\t\\t\\t\\talign_origin_x_type = \\\u0022{options.AlignOriginX.ToKv3Value()}\\\u0022\u0022 );\n\t\t\t\tsb.AppendLine( $\u0022\\t\\t\\t\\t\\t\\talign_origin_y_type = \\\u0022{options.AlignOriginY.ToKv3Value()}\\\u0022\u0022 );\n\t\t\t\tsb.AppendLine( $\u0022\\t\\t\\t\\t\\t\\talign_origin_z_type = \\\u0022{options.AlignOriginZ.ToKv3Value()}\\\u0022\u0022 );\n\t\t\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\tfaceMergeAngle = 20.0\u0022 );\n\t\t\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\tmaxHullVertices = 32\u0022 );\n\t\t\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t\\thull_mode = \\\u0022HullPerElement\\\u0022\u0022 );\n\t\t\t\tbreak;\n\t\t}\n\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t\\t},\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t\\t]\u0022 );\n\t\tsb.AppendLine( \u0022\\t\\t\\t},\u0022 );\n\t}\n\n\tprivate static string FormatNumber( float value )\n\t{\n\t\treturn value.ToString( \u00220.0########\u0022, CultureInfo.InvariantCulture );\n\t}\n}\n"},{"Ident":"bluedock.modelpro","Path":"Editor/FileNodes.cs","FileName":"FileNodes.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340317,"Code":"using System;\nusing System.IO;\n\nnamespace ModelPro.Editor;\n\n/// \u003Csummary\u003EA tree node representing a folder on disk.\u003C/summary\u003E\npublic class FolderNode : TreeNode\u003CDirectoryInfo\u003E\n{\n\tpublic string FullPath { get; }\n\tAction\u003Cstring\u003E _onSelected;\n\n\tpublic override string Name =\u003E System.IO.Path.GetFileName( FullPath );\n\n\tpublic FolderNode( string fullPath, Action\u003Cstring\u003E onSelected ) : base( new DirectoryInfo( fullPath ) )\n\t{\n\t\tFullPath = fullPath;\n\t\t_onSelected = onSelected;\n\t\tHeight = Theme.RowHeight;\n\t}\n\n\tprotected override void BuildChildren()\n\t{\n\t\tClear();\n\n\t\tforeach ( var dir in Directory.GetDirectories( FullPath )\n\t\t\t\t\t .OrderBy( x =\u003E x, StringComparer.OrdinalIgnoreCase ) )\n\t\t{\n\t\t\tAddItem( new FolderNode( dir, _onSelected ) );\n\t\t}\n\t}\n\n\tpublic override void OnSelectionChanged( bool state )\n\t{\n\t\tif ( state )\n\t\t\t_onSelected?.Invoke( FullPath );\n\t}\n\n\tpublic override void OnPaint( VirtualWidget item )\n\t{\n\t\tPaintSelection( item );\n\n\t\tvar rect = item.Rect;\n\n\t\tPaint.SetPen( Theme.Yellow );\n\t\tPaint.DrawIcon( rect, \u0022folder\u0022, 18, TextFlag.LeftCenter );\n\n\t\trect.Left \u002B= 24;\n\t\tPaint.SetPen( Theme.Text );\n\t\tPaint.SetDefaultFont();\n\t\tPaint.DrawText( rect, Name, TextFlag.LeftCenter );\n\t}\n\n\tpublic override string GetTooltip()\n\t{\n\t\treturn FullPath;\n\t}\n}\n\n/// \u003Csummary\u003EA tree node representing a file.\u003C/summary\u003E\npublic class FileNode : TreeNode\u003Cstring\u003E\n{\n\tpublic string FullPath { get; }\n\tpublic int MeshEntryCount { get; }\n\n\tpublic override string Name =\u003E System.IO.Path.GetFileName( FullPath );\n\n\tpublic FileNode( string fullPath, int meshEntryCount ) : base( fullPath )\n\t{\n\t\tFullPath = fullPath;\n\t\tMeshEntryCount = meshEntryCount;\n\t\tHeight = Theme.RowHeight;\n\t}\n\n\tpublic override void OnPaint( VirtualWidget item )\n\t{\n\t\tPaintSelection( item );\n\n\t\tvar rect = item.Rect;\n\n\t\tPaint.SetPen( Theme.Text );\n\t\tPaint.SetDefaultFont();\n\t\tPaint.DrawText( rect, MeshEntryCount \u003E 0 ? $\u0022{Name}  ({MeshEntryCount} meshes)\u0022 : Name, TextFlag.LeftCenter );\n\t}\n\n\tpublic override string GetTooltip()\n\t{\n\t\treturn FullPath;\n\t}\n}\n"},{"Ident":"bluedock.modelpro","Path":".obj/__compiler_extra.cs","FileName":"__compiler_extra.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":340317,"Code":"global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonTitle\u0022, \u0022Model Pro\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonIdent\u0022, \u0022modelpro\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022OrgIdent\u0022, \u0022bluedock\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022Ident\u0022, \u0022bluedock.modelpro\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineVersion\u0022, \u002228\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineMinorVersion\u0022, \u00221\u0022 )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \u0022.NETCoreApp,Version=v9.0\u0022, FrameworkDisplayName = \u0022.NET 9.0\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022CompileTime\u0022, \u00222026-08-09T08:00:58.3436594Z\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\u00220.0.113.0\u0022)]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\u00220.0.113.0\u0022)]"},{"Ident":"bluedock.modelpro","Path":"UnitTests/Kv3Tests.cs","FileName":"Kv3Tests.cs","PackageType":"library","CodeKind":"UnitTest","AssetVersionId":340317,"Code":"using System;\nusing Microsoft.VisualStudio.TestTools.UnitTesting;\nusing ModelPro.Vmdl;\n\n[TestClass]\npublic class Kv3Tests\n{\n\tconst string SampleVmdl = \u0022\u0022\u0022\n\t\u003C!-- kv3 encoding:text:version{e21c7f3c-8a33-41c5-9977-a76d3a32aa0d} format:modeldoc30:version{8c2d7a91-9c42-4bf0-883a-5a3b1762d4f1} --\u003E\n\t{\n\t\trootNode = \n\t\t{\n\t\t\t_class = \u0022RootNode\u0022\n\t\t\tchildren = \n\t\t\t[\n\t\t\t\t{\n\t\t\t\t\t_class = \u0022RenderMeshList\u0022\n\t\t\t\t\tchildren = \n\t\t\t\t\t[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t_class = \u0022RenderMeshFile\u0022\n\t\t\t\t\t\t\tname = \u0022Torso_LOD0\u0022\n\t\t\t\t\t\t\tchildren = \n\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t_class = \u0022RenderMeshMarkup\u0022\n\t\t\t\t\t\t\t\t\tuse_expensive_tangents = true\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\tfilename = \u0022models/citizen/citizen.fbx\u0022\n\t\t\t\t\t\t\timport_translation = [ 0.0, 0.0, 0.0 ]\n\t\t\t\t\t\t\timport_rotation = [ 0.0, 0.0, 0.0 ]\n\t\t\t\t\t\t\timport_scale = 1.0\n\t\t\t\t\t\t\talign_origin_x_type = \u0022None\u0022\n\t\t\t\t\t\t\talign_origin_y_type = \u0022None\u0022\n\t\t\t\t\t\t\talign_origin_z_type = \u0022None\u0022\n\t\t\t\t\t\t\tparent_bone = \u0022\u0022\n\t\t\t\t\t\t\timport_filter = \n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\texclude_by_default = true\n\t\t\t\t\t\t\t\texception_list = \n\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\u0022CitizenTorso_LOD0\u0022,\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t_class = \u0022RenderMeshFile\u0022\n\t\t\t\t\t\t\tname = \u0022Arms_LOD0\u0022\n\t\t\t\t\t\t\tfilename = \u0022models/citizen/citizen_arms.fbx\u0022\n\t\t\t\t\t\t\timport_scale = 1.0\n\t\t\t\t\t\t\talign_origin_x_type = \u0022None\u0022\n\t\t\t\t\t\t\talign_origin_y_type = \u0022None\u0022\n\t\t\t\t\t\t\talign_origin_z_type = \u0022None\u0022\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t]\n\t\t\tmodel_archetype = \u0022\u0022\n\t\t}\n\t}\n\t\u0022\u0022\u0022;\n\n\t[TestMethod]\n\tpublic void FindsMeshEntries()\n\t{\n\t\tvar doc = Kv3Document.Parse( SampleVmdl );\n\t\tAssert.AreEqual( 2, doc.FindObjects( \u0022RenderMeshFile\u0022 ).Count );\n\t\tAssert.AreEqual( 1, doc.FindObjects( \u0022RenderMeshList\u0022 ).Count );\n\t}\n\n\t[TestMethod]\n\tpublic void AppliesScaleAndAlign()\n\t{\n\t\tvar editor = VmdlBulkEditor.Load( SampleVmdl );\n\t\tAssert.AreEqual( 2, editor.MeshEntryCount );\n\n\t\tvar props = new MeshEntryProperties\n\t\t{\n\t\t\tImportScale = 0.5f,\n\t\t\tAlignOriginX = AlignOrigin.Center,\n\t\t\tAlignOriginY = AlignOrigin.Mins,\n\t\t\tAlignOriginZ = AlignOrigin.Maxs\n\t\t};\n\n\t\tAssert.IsTrue( editor.Apply( props ) );\n\n\t\tvar result = Kv3Document.Parse( editor.Source );\n\t\tvar entries = result.FindObjects( \u0022RenderMeshFile\u0022 );\n\t\tAssert.AreEqual( 2, entries.Count );\n\n\t\tforeach ( var entry in entries )\n\t\t{\n\t\t\tAssert.AreEqual( \u00220.5\u0022, (entry.FindField( \u0022import_scale\u0022 ).Value as Kv3Scalar)?.Value );\n\t\t\tAssert.AreEqual( \u0022Center\u0022, (entry.FindField( \u0022align_origin_x_type\u0022 ).Value as Kv3Scalar)?.Value );\n\t\t\tAssert.AreEqual( \u0022Mins\u0022, (entry.FindField( \u0022align_origin_y_type\u0022 ).Value as Kv3Scalar)?.Value );\n\t\t\tAssert.AreEqual( \u0022Maxs\u0022, (entry.FindField( \u0022align_origin_z_type\u0022 ).Value as Kv3Scalar)?.Value );\n\t\t}\n\t}\n\n\t[TestMethod]\n\tpublic void ConvertsScaleUnits()\n\t{\n\t\t// The value in the field is the import_scale multiplier written to the vmdl.\n\t\tAssert.AreEqual( 1.0f, new ScaleInput { Value = 1, Unit = ScaleUnit.Inches }.ToImportScale(), 0.0001f );\n\t\tAssert.AreEqual( 0.3937f, new ScaleInput { Value = 0.3937f, Unit = ScaleUnit.Centimeters }.ToImportScale(), 0.0001f );\n\n\t\t// Switching 1 inch to cm re-expresses it as 0.3937 (1 cm = 0.3937 inches).\n\t\tvar inches = new ScaleInput { Value = 1, Unit = ScaleUnit.Inches };\n\t\tvar cm = inches.ConvertTo( ScaleUnit.Centimeters );\n\t\tAssert.AreEqual( 0.3937f, cm.Value, 0.0001f );\n\t\tAssert.AreEqual( 0.3937f, cm.ToImportScale(), 0.0001f );\n\n\t\t// A cm model (import_scale 0.3937) reads back as itself.\n\t\tvar fromCm = ScaleInput.FromImportScale( 0.3937f, ScaleUnit.Centimeters );\n\t\tAssert.AreEqual( 0.3937f, fromCm.Value, 0.0001f );\n\t\tAssert.AreEqual( 0.3937f, fromCm.ToImportScale(), 0.0001f );\n\t}\n\n\t[TestMethod]\n\tpublic void NoOpWhenValuesMatch()\n\t{\n\t\tvar editor = VmdlBulkEditor.Load( SampleVmdl );\n\n\t\tvar props = new MeshEntryProperties\n\t\t{\n\t\t\tImportScale = 1.0f,\n\t\t\tAlignOriginX = AlignOrigin.None,\n\t\t\tAlignOriginY = AlignOrigin.None,\n\t\t\tAlignOriginZ = AlignOrigin.None\n\t\t};\n\n\t\tAssert.IsFalse( editor.Apply( props ) );\n\t\tAssert.AreEqual( SampleVmdl, editor.Source );\n\t}\n\n\t[TestMethod]\n\tpublic void ReadsFirstEntryProperties()\n\t{\n\t\tvar editor = VmdlBulkEditor.Load( SampleVmdl );\n\t\tvar props = editor.ReadFirstEntryProperties();\n\n\t\tAssert.AreEqual( 1.0f, props.ImportScale );\n\t\tAssert.AreEqual( AlignOrigin.None, props.AlignOriginX );\n\t\tAssert.AreEqual( AlignOrigin.None, props.AlignOriginY );\n\t\tAssert.AreEqual( AlignOrigin.None, props.AlignOriginZ );\n\t}\n}\n"},{"Ident":"bluedock.modelpro","Path":"UnitTests/MaterialGroupTests.cs","FileName":"MaterialGroupTests.cs","PackageType":"library","CodeKind":"UnitTest","AssetVersionId":340317,"Code":"using System.Linq;\nusing Microsoft.VisualStudio.TestTools.UnitTesting;\nusing ModelPro.Vmdl;\n\n[TestClass]\npublic class MaterialGroupTests\n{\n\tconst string WithMaterial = \u0022\u0022\u0022\n\t{\n\t\trootNode = \n\t\t{\n\t\t\t_class = \u0022RootNode\u0022\n\t\t\tchildren = \n\t\t\t[\n\t\t\t\t{\n\t\t\t\t\t_class = \u0022MaterialGroupList\u0022\n\t\t\t\t\tchildren = \n\t\t\t\t\t[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t_class = \u0022DefaultMaterialGroup\u0022\n\t\t\t\t\t\t\tremaps = [  ]\n\t\t\t\t\t\t\tuse_global_default = true\n\t\t\t\t\t\t\tglobal_default_material = \u0022materials/old.vmat\u0022\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t_class = \u0022PhysicsShapeList\u0022\n\t\t\t\t\tchildren = \n\t\t\t\t\t[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t_class = \u0022PhysicsHullFromRender\u0022\n\t\t\t\t\t\t\tparent_bone = \u0022\u0022\n\t\t\t\t\t\t\tsurface_prop = \u0022default\u0022\n\t\t\t\t\t\t\tcollision_tags = \u0022solid\u0022\n\t\t\t\t\t\t\tfaceMergeAngle = 20.0\n\t\t\t\t\t\t\tmaxHullVertices = 32\n\t\t\t\t\t\t\thull_mode = \u0022HullPerElement\u0022\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t_class = \u0022RenderMeshList\u0022\n\t\t\t\t\tchildren = \n\t\t\t\t\t[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t_class = \u0022RenderMeshFile\u0022\n\t\t\t\t\t\t\tfilename = \u0022models/foo.fbx\u0022\n\t\t\t\t\t\t\timport_scale = 1.0\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t]\n\t\t}\n\t}\n\t\u0022\u0022\u0022;\n\n\t[TestMethod]\n\tpublic void SetsDefaultMaterial()\n\t{\n\t\tvar editor = VmdlBulkEditor.Load( WithMaterial );\n\t\tAssert.AreEqual( \u0022materials/old.vmat\u0022, editor.ReadDefaultMaterial() );\n\n\t\tvar props = new MeshEntryProperties { GlobalDefaultMaterial = \u0022materials/new.vmat\u0022 };\n\t\tAssert.IsTrue( editor.Apply( props ) );\n\n\t\tvar parsed = Kv3Document.Parse( editor.Source );\n\t\tvar group = parsed.FindObjects( \u0022DefaultMaterialGroup\u0022 ).Single();\n\t\tAssert.AreEqual( \u0022materials/new.vmat\u0022, ((Kv3Scalar)group.FindField( \u0022global_default_material\u0022 ).Value).Value );\n\t\tAssert.AreEqual( \u0022true\u0022, ((Kv3Scalar)group.FindField( \u0022use_global_default\u0022 ).Value).Value );\n\t}\n\n\t[TestMethod]\n\tpublic void NullMaterialLeavesUnchanged()\n\t{\n\t\tvar editor = VmdlBulkEditor.Load( WithMaterial );\n\n\t\tvar props = new MeshEntryProperties { ImportScale = 2.0f };\n\t\tAssert.IsTrue( editor.Apply( props ) );\n\n\t\tvar parsed = Kv3Document.Parse( editor.Source );\n\t\tvar group = parsed.FindObjects( \u0022DefaultMaterialGroup\u0022 ).Single();\n\t\tAssert.AreEqual( \u0022materials/old.vmat\u0022, ((Kv3Scalar)group.FindField( \u0022global_default_material\u0022 ).Value).Value );\n\t}\n\n\t[TestMethod]\n\tpublic void AlignOriginAppliesToCollision()\n\t{\n\t\tvar editor = VmdlBulkEditor.Load( WithMaterial );\n\n\t\tvar props = new MeshEntryProperties\n\t\t{\n\t\t\tAlignOriginX = AlignOrigin.BoundsCenter,\n\t\t\tAlignOriginY = AlignOrigin.BoundsCenter,\n\t\t\tAlignOriginZ = AlignOrigin.BoundsMin\n\t\t};\n\n\t\tAssert.IsTrue( editor.Apply( props ) );\n\n\t\tvar parsed = Kv3Document.Parse( editor.Source );\n\n\t\t// Render mesh\n\t\tvar mesh = parsed.FindObjects( \u0022RenderMeshFile\u0022 ).Single();\n\t\tAssert.AreEqual( \u0022BoundsCenter\u0022, ((Kv3Scalar)mesh.FindField( \u0022align_origin_x_type\u0022 ).Value).Value );\n\t\tAssert.AreEqual( \u0022BoundsMin\u0022, ((Kv3Scalar)mesh.FindField( \u0022align_origin_z_type\u0022 ).Value).Value );\n\n\t\t// Collision shape must match\n\t\tvar hull = parsed.FindObjects( \u0022PhysicsHullFromRender\u0022 ).Single();\n\t\tAssert.AreEqual( \u0022BoundsCenter\u0022, ((Kv3Scalar)hull.FindField( \u0022align_origin_x_type\u0022 ).Value).Value );\n\t\tAssert.AreEqual( \u0022BoundsCenter\u0022, ((Kv3Scalar)hull.FindField( \u0022align_origin_y_type\u0022 ).Value).Value );\n\t\tAssert.AreEqual( \u0022BoundsMin\u0022, ((Kv3Scalar)hull.FindField( \u0022align_origin_z_type\u0022 ).Value).Value );\n\t}\n}\n"},{"Ident":"bluedock.modelpro","Path":"Assembly.cs","FileName":"Assembly.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":340317,"Code":"global using Sandbox;\nglobal using System.Collections.Generic;\nglobal using System.Linq;\n"}]}