[{"data":1,"prerenderedAt":4776},["ShallowReactive",2],{"work-at-15":3,"work-surround":42},{"id":4,"title":5,"body":6,"description":12,"excerpt":23,"extension":24,"headerImage":25,"images":26,"meta":31,"navigation":33,"order":34,"password":23,"path":35,"seo":36,"size":21,"software":37,"stem":40,"__hash__":41},"work\u002Fwork\u002Fat-15.md","AT-15",{"type":7,"value":8,"toc":19},"minimark",[9,13,16],[10,11,12],"p",{},"AT-15 Assault Rifle with skins",[10,14,15],{},"Based on the AR-15 platform",[10,17,18],{},"Used in Miscreated",{"title":20,"searchDepth":21,"depth":21,"links":22},"",2,[],null,"md","\u002Fimages\u002Fwork\u002Fat-15-header.png",[27,28,29,30,25],"\u002Fimages\u002Fwork\u002Fat-15-tech-camo.png","\u002Fimages\u002Fwork\u002Fat-15-spray-camo.png","\u002Fimages\u002Fwork\u002Fat-15-plain.png","\u002Fimages\u002Fwork\u002Fat-15-other-side.png",{"date":32},"04\u002F18\u002F2022",true,21,"\u002Fwork\u002Fat-15",{"title":5,"description":12},[38,39],"modo","substancePainter","work\u002Fat-15","4mQ8LYAwsnTG8trFTRpQjP5xyY2oDHp7RQvxEnf0vow",[43,78,93,351,416,449,474,503,558,589,684,727,753,1833,1859,1921,1951,2014,2130,2154,2206,2227,2248,2275,2638,2668,4669,4707,4735],{"id":44,"title":45,"body":46,"description":65,"excerpt":23,"extension":24,"headerImage":66,"images":67,"meta":68,"navigation":33,"order":70,"password":23,"path":71,"seo":72,"size":21,"software":73,"stem":76,"__hash__":77},"work\u002Fwork\u002Far-map.md","AR Map",{"type":7,"value":47,"toc":63},[48,57,60],[10,49,50,51,56],{},"Built for ",[52,53,55],"a",{"href":54},"\u002Fwork\u002Fheartland","The Division: Heartland",", this was created from the game world through a process and then ingested into Houdini where it was used to update the AR map.",[10,58,59],{},"This was used on the terrain and the buildings. The building outlines were generated in Houdini.",[10,61,62],{},"This was used in game as the map to start missions.",{"title":20,"searchDepth":21,"depth":21,"links":64},[],"Built for The Division: Heartland, this was created from the game world through a process and then ingested into Houdini where it was used to update the AR map.","\u002Fimages\u002Fwork\u002Far-map-header.png",[66],{"date":69},"02\u002F01\u002F2026",5,"\u002Fwork\u002Far-map",{"title":45,"description":65},[74,75],"houdini","snowdrop","work\u002Far-map","LbLaQQD6AlF4lBs9ecslrrAenN6Sx9AoEr87Xfs0Gus",{"id":4,"title":5,"body":79,"description":12,"excerpt":23,"extension":24,"headerImage":25,"images":89,"meta":90,"navigation":33,"order":34,"password":23,"path":35,"seo":91,"size":21,"software":92,"stem":40,"__hash__":41},{"type":7,"value":80,"toc":87},[81,83,85],[10,82,12],{},[10,84,15],{},[10,86,18],{},{"title":20,"searchDepth":21,"depth":21,"links":88},[],[27,28,29,30,25],{"date":32},{"title":5,"description":12},[38,39],{"id":94,"title":95,"body":96,"description":333,"excerpt":23,"extension":24,"headerImage":334,"images":335,"meta":341,"navigation":33,"order":342,"password":23,"path":343,"seo":344,"size":132,"software":345,"stem":349,"__hash__":350},"work\u002Fwork\u002Fatlas.md","Atlas",{"type":7,"value":97,"toc":328},[98,105,108,111,114,119,181,185,274,278,324],[10,99,50,100,104],{},[52,101,103],{"href":102},"\u002Fwork\u002Fmiscreated","Miscreated",", Atlas Parser\u002FViewer is a Python\u002FElectron\u002FTypeScript project used to collect metadata from game files and create actionable data points.",[10,106,107],{},"The parser is written in python and can parse all of the game files and extract specific data points from those files it then inserts into a sqlite3 database.",[10,109,110],{},"This then allows artists to open up Atlas Viewer a program written using Electron\u002FTypescript\u002FReact to then view the metadata collected from the game files and run SQL queries to gather data to be used to help target performance issues or general problems.",[10,112,113],{},"Some common queries that are used are:",[115,116,118],"h3",{"id":117},"find-any-images-that-are-larger-than-2k-and-do-not-have-a-reduction-setting","Find any images that are larger than 2k and do not have a reduction setting.",[120,121,125],"pre",{"className":122,"code":123,"language":124,"meta":20,"style":20},"language-sql shiki shiki-themes github-light github-dark","SELECT *\nFROM textures\nWHERE image_width > 2048\nAND reduce LIKE \"0\"\n","sql",[126,127,128,140,149,165],"code",{"__ignoreMap":20},[129,130,133,137],"span",{"class":131,"line":132},"line",1,[129,134,136],{"class":135},"szBVR","SELECT",[129,138,139],{"class":135}," *\n",[129,141,142,145],{"class":131,"line":21},[129,143,144],{"class":135},"FROM",[129,146,148],{"class":147},"sVt8B"," textures\n",[129,150,152,155,158,161],{"class":131,"line":151},3,[129,153,154],{"class":135},"WHERE",[129,156,157],{"class":147}," image_width ",[129,159,160],{"class":135},">",[129,162,164],{"class":163},"sj4cs"," 2048\n",[129,166,168,171,174,177],{"class":131,"line":167},4,[129,169,170],{"class":135},"AND",[129,172,173],{"class":147}," reduce ",[129,175,176],{"class":135},"LIKE",[129,178,180],{"class":179},"sZZnC"," \"0\"\n",[115,182,184],{"id":183},"find-any-lights-with-really-large-radii-that-cast-shadows-these-are-very-bad","Find any lights with really large radii that cast shadows. These are very bad!",[120,186,188],{"className":122,"code":187,"language":124,"meta":20,"style":20},"SELECT `radius` , `name`, `pos`, *\nFROM `Entity`\nWHERE `EntityClass` LIKE \"Light\"\nAND CAST(`radius` as INTEGER) > 70\nORDER BY `radius` DESC;\n",[126,189,190,214,221,234,261],{"__ignoreMap":20},[129,191,192,194,197,200,203,206,209,211],{"class":131,"line":132},[129,193,136],{"class":135},[129,195,196],{"class":179}," `radius`",[129,198,199],{"class":147}," , ",[129,201,202],{"class":179},"`name`",[129,204,205],{"class":147},", ",[129,207,208],{"class":179},"`pos`",[129,210,205],{"class":147},[129,212,213],{"class":135},"*\n",[129,215,216,218],{"class":131,"line":21},[129,217,144],{"class":135},[129,219,220],{"class":179}," `Entity`\n",[129,222,223,225,228,231],{"class":131,"line":151},[129,224,154],{"class":135},[129,226,227],{"class":179}," `EntityClass`",[129,229,230],{"class":135}," LIKE",[129,232,233],{"class":179}," \"Light\"\n",[129,235,236,238,241,244,247,250,253,256,258],{"class":131,"line":167},[129,237,170],{"class":135},[129,239,240],{"class":163}," CAST",[129,242,243],{"class":147},"(",[129,245,246],{"class":179},"`radius`",[129,248,249],{"class":135}," as",[129,251,252],{"class":135}," INTEGER",[129,254,255],{"class":147},") ",[129,257,160],{"class":135},[129,259,260],{"class":163}," 70\n",[129,262,263,266,268,271],{"class":131,"line":70},[129,264,265],{"class":135},"ORDER BY",[129,267,196],{"class":179},[129,269,270],{"class":135}," DESC",[129,272,273],{"class":147},";\n",[115,275,277],{"id":276},"find-any-materials-that-are-missing-surface-types-this-causes-ugly-missing-surface-material-decals-to-pop-up","Find any materials that are missing surface types. This causes ugly missing surface material decals to pop up.",[120,279,281],{"className":122,"code":280,"language":124,"meta":20,"style":20},"SELECT filepath, id, *\nFROM Materials\nWHERE SurfaceType IS NULL and issubmat like \"True\"\n",[126,282,283,292,299],{"__ignoreMap":20},[129,284,285,287,290],{"class":131,"line":132},[129,286,136],{"class":135},[129,288,289],{"class":147}," filepath, id, ",[129,291,213],{"class":135},[129,293,294,296],{"class":131,"line":21},[129,295,144],{"class":135},[129,297,298],{"class":147}," Materials\n",[129,300,301,303,306,309,312,315,318,321],{"class":131,"line":151},[129,302,154],{"class":135},[129,304,305],{"class":147}," SurfaceType ",[129,307,308],{"class":135},"IS",[129,310,311],{"class":135}," NULL",[129,313,314],{"class":135}," and",[129,316,317],{"class":147}," issubmat ",[129,319,320],{"class":135},"like",[129,322,323],{"class":179}," \"True\"\n",[325,326,327],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":20,"searchDepth":21,"depth":21,"links":329},[330,331,332],{"id":117,"depth":151,"text":118},{"id":183,"depth":151,"text":184},{"id":276,"depth":151,"text":277},"Built for Miscreated, Atlas Parser\u002FViewer is a Python\u002FElectron\u002FTypeScript project used to collect metadata from game files and create actionable data points.","\u002Fimages\u002Fuploads\u002F5d54b95cb66dbatlas_1_37b372b660.jpg",[336,337,338,339,340],"\u002Fimages\u002Fwork\u002Faatlas-c.jpg","\u002Fimages\u002Fwork\u002Fatlas-a.png","\u002Fimages\u002Fwork\u002Fatlas-b.png","\u002Fimages\u002Fwork\u002Fatlas-d.png","\u002Fimages\u002Fwork\u002Fatlas-e.jpg",{"date":32},12,"\u002Fwork\u002Fatlas",{"title":95,"description":333},[346,347,348],"electron","python","typescript","work\u002Fatlas","kjYYpLzTefR32sA-VIwiqLMuaye0d-VANu3rBilvFZ0",{"id":352,"title":353,"body":354,"description":20,"excerpt":23,"extension":24,"headerImage":408,"images":23,"meta":409,"navigation":33,"order":410,"password":23,"path":411,"seo":412,"size":21,"software":413,"stem":414,"__hash__":415},"work\u002Fwork\u002Fawards.md","Awards",{"type":7,"value":355,"toc":403},[356,362,367,384,388,393,397],[10,357,358],{},[359,360],"img",{"alt":353,"src":361},"\u002Fimages\u002Fawards.jpg",[363,364,366],"h2",{"id":365},"eye-of-the-storm-2025","Eye of the Storm - (2025)",[10,368,369,373,374,378,379,383],{},[370,371,372],"strong",{},"Red Storm Entertainment \u002F Ubisoft"," — Recognized for spearheading the ",[52,375,377],{"href":376},"\u002Fwork\u002Frainbow-six-siege","Rainbow Six Siege"," production pipeline migration from JIRA to ",[52,380,382],{"href":381},"\u002Fwork\u002Fshotgrid","Flow Production Tracking"," alongside Diana Fitzpatrick. An 8+ month initiative that coordinated 50+ contributors and transformed how thousands of people across the production track and deliver game content.",[363,385,387],{"id":386},"eye-of-the-storm-2023","Eye of the Storm - (2023)",[10,389,390,392],{},[370,391,372],{}," — Selected by studio leadership for making an outsized impact on the studio's success. The Eye of the Storm award honors individuals whose contributions — whether through project leadership, tool innovation, or elevating the people around them — prove critical to the studio's mission.",[363,394,396],{"id":395},"collaborator-of-the-year-2021","Collaborator of the Year - (2021)",[10,398,399,402],{},[370,400,401],{},"Red Storm Entertainment"," — Chosen by a studio-wide vote as the person who best exemplified collaboration across disciplines. This peer-nominated award recognizes someone who consistently bridges teams, unblocks others, and makes everyone around them more effective.",{"title":20,"searchDepth":21,"depth":21,"links":404},[405,406,407],{"id":365,"depth":21,"text":366},{"id":386,"depth":21,"text":387},{"id":395,"depth":21,"text":396},"\u002Fimages\u002Fawards-header.jpg",{"date":69},6,"\u002Fwork\u002Fawards",{"title":353,"description":20},[],"work\u002Fawards","3CuDQORNB6lWl6_CK73JKXqH-aQBk-vTcHrGPMn8oE0",{"id":417,"title":418,"body":419,"description":423,"excerpt":23,"extension":24,"headerImage":432,"images":433,"meta":441,"navigation":33,"order":442,"password":23,"path":443,"seo":444,"size":132,"software":445,"stem":447,"__hash__":448},"work\u002Fwork\u002Fbackpacks.md","Rugged Pack",{"type":7,"value":420,"toc":430},[421,424,427],[10,422,423],{},"Rugged Backpack with material variations driven via material.",[10,425,426],{},"Used in Miscreated.",[10,428,429],{},"Created with Modo, Marvelous Designer 7, and Substance Painter.",{"title":20,"searchDepth":21,"depth":21,"links":431},[],"\u002Fimages\u002Fuploads\u002F5d547d26e0509_Rugged_Pack_Brown_header_6d0062087a.png",[434,435,436,437,438,439,440],"\u002Fimages\u002Fuploads\u002F5d54ba2c6c57d_Rugged_Pack_Camo2_2048_438544bc92.png","\u002Fimages\u002Fuploads\u002F5d54ba2c3697d_Rugged_Pack_2048_287a683318.png","\u002Fimages\u002Fuploads\u002F5d54ba2cc3897_Rugged_Pack_Camo3_2048_543cf14805.png","\u002Fimages\u002Fuploads\u002F5d54ba2d04e73_Rugged_Pack_Green_2048_75cb50d864.png","\u002Fimages\u002Fuploads\u002F5d54ba2d6f524_Rugged_Pack_Green_Camo3_2048_685e0e7862.png","\u002Fimages\u002Fuploads\u002F5d54ba2d34f57_Rugged_Pack_Green_Camo2_2048_9f07a7cfa4.png","\u002Fimages\u002Fuploads\u002F5d54ba2db3fa7_Rugged_Pack_Green_Camo4_2048_8bfbe8f9be.png",{"date":32},17,"\u002Fwork\u002Fbackpacks",{"title":418,"description":423},[38,446,39],"marvelous","work\u002Fbackpacks","YVG3qbtIOTAlcEoiCdc00Y8pyfcL5ddG3gnBKp5Xx9k",{"id":450,"title":451,"body":452,"description":456,"excerpt":23,"extension":24,"headerImage":465,"images":466,"meta":467,"navigation":33,"order":468,"password":23,"path":469,"seo":470,"size":21,"software":471,"stem":472,"__hash__":473},"work\u002Fwork\u002Fbase-parts.md","Base Parts",{"type":7,"value":453,"toc":463},[454,457,460],[10,455,456],{},"These metal towable base parts are used in Miscreated in the base building system. They are found around the world and towed back to your base.",[10,458,459],{},"They are designed to fit within a base building system using specific sizes for parts.",[10,461,462],{},"I created the base parts in modo and the metal plating textures in Substance Painter.",{"title":20,"searchDepth":21,"depth":21,"links":464},[],"\u002Fimages\u002Fuploads\u002F5d545a020a812base_parts_header_04f727b88f.png",[465],{"date":32},22,"\u002Fwork\u002Fbase-parts",{"title":451,"description":456},[38,39],"work\u002Fbase-parts","VYP7gPTsZlQl1kHqDWVb4wVjKk18OE0QuIHHgDuBpAs",{"id":475,"title":476,"body":477,"description":481,"excerpt":23,"extension":24,"headerImage":487,"images":488,"meta":496,"navigation":33,"order":497,"password":23,"path":498,"seo":499,"size":132,"software":500,"stem":501,"__hash__":502},"work\u002Fwork\u002Fbike-helmet.md","Bike Helmet",{"type":7,"value":478,"toc":485},[479,482],[10,480,481],{},"A wearable bicycle helmet used in Miscreated",[10,483,484],{},"Multiple color variations driven via material to reduce texture usage.",{"title":20,"searchDepth":21,"depth":21,"links":486},[],"\u002Fimages\u002Fuploads\u002F5d54c65961a08_Bike_Helmet_Yellow_header_37e4e9c3c4.png",[489,490,491,492,493,494,495],"\u002Fimages\u002Fuploads\u002F5d462377093bc_Bike_Helmet_Green_2048_44c98b028b.png","\u002Fimages\u002Fuploads\u002F5d462377d17b2_Bike_Helmet_Yellow_2048_6a9b998fca.png","\u002Fimages\u002Fuploads\u002F5d46237737812_Bike_Helmet_Pink_2048_9ef50d7c45.png","\u002Fimages\u002Fuploads\u002F5d46237783293_Bike_Helmet_Red_2048_7e200c0791.png","\u002Fimages\u002Fuploads\u002F5d462376d7021_Bike_Helmet_Blue_2048_2478aab0f8.png","\u002Fimages\u002Fuploads\u002F5d462377a8fd3_Bike_Helmet_White_2048_ad7b5b7b2c.png","\u002Fimages\u002Fuploads\u002F5d4623775d5b4_Bike_Helmet_Purple_2048_bea4ff764c.png",{"date":32},20,"\u002Fwork\u002Fbike-helmet",{"title":476,"description":481},[38,39],"work\u002Fbike-helmet","cT7_DOiOqjk0PPpOYiaMIlh9iSsCMunLqFU6Z1-PVPY",{"id":504,"title":505,"body":506,"description":20,"excerpt":23,"extension":24,"headerImage":549,"images":23,"meta":550,"navigation":33,"order":551,"password":552,"path":553,"seo":554,"size":132,"software":555,"stem":556,"__hash__":557},"work\u002Fwork\u002Fcat-eyes-shader.md","Cat Eyes Shader",{"type":7,"value":507,"toc":547},[508,514,520,525,528,531,536,539,544],[10,509,510],{},[359,511],{"alt":512,"src":513},"Cat Eyes","\u002Fimages\u002Fwork\u002Fcat-eyes.gif",[10,515,516,517,519],{},"Built during ",[52,518,55],{"href":54},", a shader to simulate realistic cat eyes, featuring dynamic pupil dilation and the tapetum lucidum — the reflective layer behind a cat's retina that causes that distinctive eye-shine in the dark.",[10,521,522],{},[370,523,524],{},"Pupil Dilation",[10,526,527],{},"The pupil animation is driven by a custom gradient texture generated in Houdini. Each frame of the animation was rendered as a black and white alpha mask representing the pupil at a different stage of dilation. These frames were then packed into a single texture by remapping each frame's values into a 0–256 range and stacked on top of each other, where each step along the texture encodes progressively more of the eye opening. This allowed the entire animation to be sampled in a single texture lookup at runtime, keeping the shader cheap and avoiding any additional draw calls.",[10,529,530],{},"The dilation was controlled by a single scalar value fed in from the game: the local screen brightness at the character's position. Using screen brightness was a practical compromise: it was already available on the client, inexpensive to compute, and gave a convincing enough approximation of how a real cat's eyes would respond to ambient light.",[10,532,533],{},[370,534,535],{},"Tapetum Lucidum",[10,537,538],{},"The shader also simulates the tapetum lucidum effect, the biological mirror-like layer in a cat's eye that reflects light back through the retina. In low-light conditions this produces the eerie greenish or reddish glow characteristic of cats photographed in the dark. The effect was tied to the same brightness input driving the pupil, so as the scene darkened the pupils would dilate and the tapetum glow would intensify simultaneously.",[10,540,541],{},[370,542,543],{},"Implementation",[10,545,546],{},"The shader was prototyped in HLSL to iterate quickly on the look, then migrated into Snowdrop's node-based material system for integration into the game.",{"title":20,"searchDepth":21,"depth":21,"links":548},[],"\u002Fimages\u002Fwork\u002Fcat-eyes-header.png",{"date":69},14,"sprance2026","\u002Fwork\u002Fcat-eyes-shader",{"title":505,"description":20},[74,75],"work\u002Fcat-eyes-shader","-DM2bApLXZ0mnIEFMbTTUMdzQVRzdx96GRx9zNW5njc",{"id":559,"title":560,"body":561,"description":565,"excerpt":23,"extension":24,"headerImage":577,"images":578,"meta":581,"navigation":33,"order":582,"password":23,"path":583,"seo":584,"size":21,"software":585,"stem":587,"__hash__":588},"work\u002Fwork\u002Fcrown-vic.md","Crown Vic",{"type":7,"value":562,"toc":575},[563,566,569,572],[10,564,565],{},"This was modelled after the Ford Crown Victoria.",[10,567,568],{},"I took the Hi poly and converted it to low poly using modo.",[10,570,571],{},"Created and baked all textures using Substance Painter",[10,573,574],{},"The multiple variations of this vehicle use a similar base texture with overlay textures and material variations driving all color in addition to a shared decal sheet.",{"title":20,"searchDepth":21,"depth":21,"links":576},[],"\u002Fimages\u002Fwork\u002Fcrown-vic-header.png",[579,580],"\u002Fimages\u002Fcrown-vic\u002Fpolice.jpg","\u002Fimages\u002Fcrown-vic\u002Ftaxi-blix.jpg",{"date":32},25,"\u002Fwork\u002Fcrown-vic",{"title":560,"description":565},[38,586,39],"photoshop","work\u002Fcrown-vic","x6RiLLy7jHfT7td2VUrT70dv-xeFivKUC96BXEx3DwU",{"id":590,"title":591,"body":592,"description":675,"excerpt":23,"extension":24,"headerImage":676,"images":23,"meta":677,"navigation":33,"order":678,"password":23,"path":679,"seo":680,"size":21,"software":681,"stem":682,"__hash__":683},"work\u002Fwork\u002Fdistance-vistas.md","Distance Vistas",{"type":7,"value":593,"toc":673},[594,605,610,613,618,621,635,640,643,648,651,656,659],[10,595,50,596,598,599,602],{},[52,597,55],{"href":54},", one of the biggest challenges in open-world games is making the horizon feel real — distant mountains, forests, and terrain that look like they belong to the same world the player is standing in. This system was built to solve exactly that.\n",[359,600],{"alt":591,"src":601},"\u002Fimages\u002Fwork\u002Fdistance-vistas\u002Fdv2.jpg",[359,603],{"alt":591,"src":604},"\u002Fimages\u002Fwork\u002Fdistance-vistas\u002Fdv3.jpg",[10,606,607],{},[370,608,609],{},"World Data Pipeline",[10,611,612],{},"The process started by extracting terrain data directly from the game world and ingesting it into Houdini. This gave a faithful foundation to work from, ensuring the distance vistas would match the actual topology of the game's landscape rather than being hand-sculpted separately and hoping they aligned.",[10,614,615],{},[370,616,617],{},"Procedural Terrain Texturing",[10,619,620],{},"From that heightfield I built a custom Megascans texturing SOP that drove all surface materials procedurally — slope, altitude, curvature, and other heightfield attributes were used to blend rock, soil, snow, and ground cover automatically. No manual texture painting meant iteration was fast and the results stayed consistent across the entire terrain. It also meant changing seasons was just adding a few layers and changing out different textures to generate a new seasonal texture.",[10,622,623,627,631],{},[359,624],{"alt":625,"src":626},"Texture Process Autumn","\u002Fimages\u002Fwork\u002Fdistance-vistas\u002Ftexture-process-autumn.gif",[359,628],{"alt":629,"src":630},"Texture Process Winter","\u002Fimages\u002Fwork\u002Fdistance-vistas\u002Ftexture-process.gif",[359,632],{"alt":633,"src":634},"Distance Vistas Texturing","\u002Fimages\u002Fwork\u002Fdistance-vistas\u002Fdv-texturing.jpg",[10,636,637],{},[370,638,639],{},"Biome-Driven Tree Scattering",[10,641,642],{},"The more interesting challenge was making the vegetation feel ecologically plausible. I built a custom biome system that modeled the survival conditions for different tree species — altitude tolerance, slope preference, moisture zones — and used those rules to scatter trees across the terrain the way they'd actually grow in nature. Different species clustered in the right places, thinned out at treelines, competed with each other and avoided terrain that wouldn't support them.",[10,644,645],{},[370,646,647],{},"X-Tree Shader",[10,649,650],{},"At distance, full geometry trees are impractical. The solution was low-poly cross-plane (X-tree) meshes paired with a custom shader designed to disguise their planarity. By carefully blending normals, adding parallax-style depth cues, and tuning the silhouette, the trees read as full and dense even under close scrutiny.",[10,652,653],{},[370,654,655],{},"Art-Directable SOPs",[10,657,658],{},"Finally, a set of geometry placement SOPs allowed artists to hand-place landmarks, rock formations, and other key elements on top of the procedural base — giving the team full art-direction control without losing the procedural foundation. The result blended seamlessly into the engine's terrain at the horizon line.",[10,660,661,664,667,670],{},[359,662],{"alt":591,"src":663},"\u002Fimages\u002Fwork\u002Fdistance-vistas\u002Fdv4.jpg",[359,665],{"alt":591,"src":666},"\u002Fimages\u002Fwork\u002Fdistance-vistas\u002Fdv5.jpg",[359,668],{"alt":591,"src":669},"\u002Fimages\u002Fwork\u002Fdistance-vistas\u002Fdv6.jpg",[359,671],{"alt":591,"src":672},"\u002Fimages\u002Fwork\u002Fdistance-vistas\u002Fdv1.jpg",{"title":20,"searchDepth":21,"depth":21,"links":674},[],"Built for The Division: Heartland, one of the biggest challenges in open-world games is making the horizon feel real — distant mountains, forests, and terrain that look like they belong to the same world the player is standing in. This system was built to solve exactly that.\n","\u002Fimages\u002Fwork\u002Fdistance-vista-header.png",{"date":69},8,"\u002Fwork\u002Fdistance-vistas",{"title":591,"description":675},[74,75],"work\u002Fdistance-vistas","KjMsV2fsjA28D1R2BIBEKbNtXb9RjyGESyxCdn4JKfg",{"id":685,"title":686,"body":687,"description":718,"excerpt":23,"extension":24,"headerImage":719,"images":23,"meta":720,"navigation":33,"order":721,"password":23,"path":722,"seo":723,"size":132,"software":724,"stem":725,"__hash__":726},"work\u002Fwork\u002Feggbot.md","Procedural Egg Bot",{"type":7,"value":688,"toc":716},[689,697,707,710,713],[10,690,691,692,696],{},"This was just a fun little project to try out the Houdini ",[52,693,695],{"href":694},"\u002Fblog\u002Fmodeler-2025-hotkeys","Modeler 2025 Hotkeys"," and toolset.",[10,698,699,700,706],{},"I wanted to try and make a little egg bot I saw on a ",[52,701,705],{"href":702,"rel":703},"https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=W76eV9hMC2M",[704],"nofollow","YouTube"," video.",[10,708,709],{},"I can adjust the size of many of the different objects on the egg bot and the model will update to place bolts, springs, and seams.",[359,711],{"src":712},"\u002Fimages\u002Fwork\u002Fegg-bot-big.png",[359,714],{"src":715},"\u002Fimages\u002Fwork\u002Fegg-bot-big-back.png",{"title":20,"searchDepth":21,"depth":21,"links":717},[],"This was just a fun little project to try out the Houdini Modeler 2025 Hotkeys and toolset.","\u002Fimages\u002Fwork\u002Feggbot.png",{"date":69},16,"\u002Fwork\u002Feggbot",{"title":686,"description":718},[74],"work\u002Feggbot","hIx69ZeUEC4MMUbqTMGR0BPFwRn2CbeReipiMUcSa28",{"id":728,"title":729,"body":730,"description":734,"excerpt":23,"extension":24,"headerImage":743,"images":744,"meta":745,"navigation":33,"order":746,"password":23,"path":747,"seo":748,"size":132,"software":749,"stem":751,"__hash__":752},"work\u002Fwork\u002Ffence-tool.md","Fence Tool",{"type":7,"value":731,"toc":741},[732,735,738],[10,733,734],{},"A Houdini Digital Asset to procedurally create many different types of fences.",[10,736,737],{},"Currently supported are Residential Vinyl Fence, A Chain Link fence with Barbed wire, and a Farm Fence",[10,739,740],{},"The HDA contains many different parameters to create gaps in the fence, adjust sizes, and the sag of wires including many other options.",{"title":20,"searchDepth":21,"depth":21,"links":742},[],"\u002Fimages\u002Fuploads\u002F5d5466e8afe46fence_tool_header_e7790380f9.png",[743],{"date":32},19,"\u002Fwork\u002Ffence-tool",{"title":729,"description":734},[74,347,750],"unreal","work\u002Ffence-tool","oF34hvh7ytrRa5VQH-cjnkNIVj4_IkwgbXj6cL6FxCs",{"id":754,"title":755,"body":756,"description":20,"excerpt":23,"extension":24,"headerImage":1823,"images":23,"meta":1824,"navigation":33,"order":1053,"password":23,"path":1826,"seo":1827,"size":132,"software":1828,"stem":1831,"__hash__":1832},"work\u002Fwork\u002Fgecs.md","GECS - Entity Component System",{"type":7,"value":757,"toc":1804},[758,762,771,792,796,854,858,861,865,869,876,880,890,894,925,930,934,1552,1556,1589,1593,1598,1608,1612,1652,1656,1697,1701,1733,1737,1769,1773,1782,1785,1791,1801],[363,759,761],{"id":760},"entity-component-system-for-godot-4x","Entity Component System for Godot 4.x",[763,764,765],"blockquote",{},[10,766,767],{},[52,768,769],{"href":769,"rel":770},"https:\u002F\u002Fgithub.com\u002Fcsprance\u002Fgecs",[704],[10,772,773,774,205,778,205,782,786,787,791],{},"Build scalable, maintainable games with clean separation of data and logic. GECS integrates seamlessly with Godot's node system while providing powerful query-based entity filtering. Check out the blog series for deep dives: ",[52,775,777],{"href":776},"\u002Fblog\u002Fgecs","Intro",[52,779,781],{"href":780},"\u002Fblog\u002Fgecs-entities","Entities & Components",[52,783,785],{"href":784},"\u002Fblog\u002Fgecs-systems-queries","Systems & Queries",", and ",[52,788,790],{"href":789},"\u002Fblog\u002Fgecs-relationships","Relationships",".",[363,793,795],{"id":794},"key-features","Key Features",[797,798,799,807,814,821,828,835,842],"ul",{},[800,801,802,803,806],"li",{},"🎯 ",[370,804,805],{},"Godot Integration"," - Works with nodes, scenes, and editor",[800,808,809,810,813],{},"🚀 ",[370,811,812],{},"High Performance"," - Optimized queries with automatic caching",[800,815,816,817,820],{},"🔧 ",[370,818,819],{},"Flexible Queries"," - Find entities by components, relationships, or properties",[800,822,823,824,827],{},"🔍 ",[370,825,826],{},"Debug Viewer"," - Real-time inspection and performance monitoring",[800,829,830,831,834],{},"📦 ",[370,832,833],{},"Editor Support"," - Visual component editing and scene integration",[800,836,837,838,841],{},"🎮 ",[370,839,840],{},"Battle Tested"," - Used in games being actively developed",[800,843,844,845,848,849],{},"🌐 ",[370,846,847],{},"Multiplayer"," - GECS goes Multiplayer! Check out the ",[52,850,853],{"href":851,"rel":852},"https:\u002F\u002Fgithub.com\u002Fcsprance\u002Fgecs\u002Ftree\u002Fmain\u002Faddons\u002Fgecs\u002Fnetwork\u002FREADME.md",[704],"GECS Network Module",[363,855,857],{"id":856},"requirements","Requirements",[10,859,860],{},"Godot 4.x (tested with 4.6+)",[363,862,864],{"id":863},"installation","Installation",[115,866,868],{"id":867},"option-a-godot-asset-library","Option A: Godot Asset Library",[10,870,871,872,875],{},"Search for ",[370,873,874],{},"\"GECS\""," in the Godot editor AssetLib tab and click Install.",[115,877,879],{"id":878},"option-b-manual-copy","Option B: Manual Copy",[10,881,882,883,886,887,791],{},"Download the release zip, copy ",[126,884,885],{},"addons\u002Fgecs\u002F"," into your project, then enable the plugin in ",[370,888,889],{},"Project Settings > Plugins",[115,891,893],{"id":892},"option-c-git-submodule","Option C: Git Submodule",[120,895,899],{"className":896,"code":897,"language":898,"meta":20,"style":20},"language-bash shiki shiki-themes github-light github-dark","git submodule add -b release-v6.8.1 https:\u002F\u002Fgithub.com\u002Fcsprance\u002Fgecs.git addons\u002Fgecs\n","bash",[126,900,901],{"__ignoreMap":20},[129,902,903,907,910,913,916,919,922],{"class":131,"line":132},[129,904,906],{"class":905},"sScJk","git",[129,908,909],{"class":179}," submodule",[129,911,912],{"class":179}," add",[129,914,915],{"class":163}," -b",[129,917,918],{"class":179}," release-v6.8.1",[129,920,921],{"class":179}," https:\u002F\u002Fgithub.com\u002Fcsprance\u002Fgecs.git",[129,923,924],{"class":179}," addons\u002Fgecs\n",[10,926,927,928,791],{},"Then enable the plugin in ",[370,929,889],{},[363,931,933],{"id":932},"quick-start","Quick Start",[120,935,939],{"className":936,"code":937,"language":938,"meta":20,"style":20},"language-gdscript shiki shiki-themes github-light github-dark","# All component properties need a default value or Godot will error on export\n\n# Pattern 1: @export var with default (no constructor needed)\nclass_name C_Health extends Component\n@export var max_health: int = 100\n@export var current_health: int = 100\n\n# Pattern 2: _init() with parameter AND a default on the property\nclass_name C_Velocity extends Component\n@export var direction: Vector3 = Vector3.ZERO\nfunc _init(v: Vector3 = Vector3.ZERO) -> void:\n    direction = v\n\n# Create entities and add components\nvar player = Entity.new()\nplayer.add_component(C_Health.new())\nplayer.add_component(C_Velocity.new(Vector3(5, 0, 0)))\n\nvar target = Entity.new()\ntarget.add_component(C_Health.new())\ntarget.add_component(C_Velocity.new(Vector3(-5, 0, 0)))\n\n# Add entities to the world\nECS.world.add_entity(player)\nECS.world.add_entity(target)\n\n# Add relationships between entities\nplayer.add_relationship(Relationship.new(C_AllyTo.new(), target))\n\n# Systems define which entities to process\nclass_name VelocitySystem extends System\n\nfunc query() -> QueryBuilder:\n    return q.with_all([C_Velocity])\n\nfunc process(entities: Array[Entity], components: Array, delta: float) -> void:\n    for entity in entities:\n        var vel := entity.get_component(C_Velocity) as C_Velocity\n        entity.position += vel.direction * delta\n\n# Register the system and start processing\nECS.world.add_system(VelocitySystem.new())\n\nfunc _process(delta: float) -> void:\n    ECS.process(delta)\n","gdscript",[126,940,941,947,952,957,971,991,1006,1011,1016,1028,1051,1085,1096,1101,1106,1128,1144,1176,1181,1198,1211,1242,1246,1252,1267,1278,1283,1289,1314,1319,1325,1338,1343,1361,1376,1381,1420,1435,1462,1480,1485,1491,1512,1517,1538],{"__ignoreMap":20},[129,942,943],{"class":131,"line":132},[129,944,946],{"class":945},"sJ8bj","# All component properties need a default value or Godot will error on export\n",[129,948,949],{"class":131,"line":21},[129,950,951],{"emptyLinePlaceholder":33},"\n",[129,953,954],{"class":131,"line":151},[129,955,956],{"class":945},"# Pattern 1: @export var with default (no constructor needed)\n",[129,958,959,962,965,968],{"class":131,"line":167},[129,960,961],{"class":135},"class_name",[129,963,964],{"class":905}," C_Health",[129,966,967],{"class":135}," extends",[129,969,970],{"class":905}," Component\n",[129,972,973,976,979,982,985,988],{"class":131,"line":70},[129,974,975],{"class":905},"@export",[129,977,978],{"class":135}," var",[129,980,981],{"class":147}," max_health: ",[129,983,984],{"class":905},"int",[129,986,987],{"class":135}," =",[129,989,990],{"class":163}," 100\n",[129,992,993,995,997,1000,1002,1004],{"class":131,"line":410},[129,994,975],{"class":905},[129,996,978],{"class":135},[129,998,999],{"class":147}," current_health: ",[129,1001,984],{"class":905},[129,1003,987],{"class":135},[129,1005,990],{"class":163},[129,1007,1009],{"class":131,"line":1008},7,[129,1010,951],{"emptyLinePlaceholder":33},[129,1012,1013],{"class":131,"line":678},[129,1014,1015],{"class":945},"# Pattern 2: _init() with parameter AND a default on the property\n",[129,1017,1019,1021,1024,1026],{"class":131,"line":1018},9,[129,1020,961],{"class":135},[129,1022,1023],{"class":905}," C_Velocity",[129,1025,967],{"class":135},[129,1027,970],{"class":905},[129,1029,1031,1033,1035,1038,1041,1043,1046,1048],{"class":131,"line":1030},10,[129,1032,975],{"class":905},[129,1034,978],{"class":135},[129,1036,1037],{"class":147}," direction: ",[129,1039,1040],{"class":905},"Vector3",[129,1042,987],{"class":135},[129,1044,1045],{"class":905}," Vector3",[129,1047,791],{"class":147},[129,1049,1050],{"class":163},"ZERO\n",[129,1052,1054,1057,1060,1063,1065,1067,1069,1071,1074,1076,1079,1082],{"class":131,"line":1053},11,[129,1055,1056],{"class":135},"func",[129,1058,1059],{"class":905}," _init",[129,1061,1062],{"class":147},"(v: ",[129,1064,1040],{"class":905},[129,1066,987],{"class":135},[129,1068,1045],{"class":905},[129,1070,791],{"class":147},[129,1072,1073],{"class":163},"ZERO",[129,1075,255],{"class":147},[129,1077,1078],{"class":135},"->",[129,1080,1081],{"class":905}," void",[129,1083,1084],{"class":147},":\n",[129,1086,1087,1090,1093],{"class":131,"line":342},[129,1088,1089],{"class":147},"    direction ",[129,1091,1092],{"class":135},"=",[129,1094,1095],{"class":147}," v\n",[129,1097,1099],{"class":131,"line":1098},13,[129,1100,951],{"emptyLinePlaceholder":33},[129,1102,1103],{"class":131,"line":551},[129,1104,1105],{"class":945},"# Create entities and add components\n",[129,1107,1109,1112,1115,1117,1120,1122,1125],{"class":131,"line":1108},15,[129,1110,1111],{"class":135},"var",[129,1113,1114],{"class":147}," player ",[129,1116,1092],{"class":135},[129,1118,1119],{"class":905}," Entity",[129,1121,791],{"class":147},[129,1123,1124],{"class":905},"new",[129,1126,1127],{"class":147},"()\n",[129,1129,1130,1133,1136,1139,1141],{"class":131,"line":721},[129,1131,1132],{"class":147},"player.",[129,1134,1135],{"class":905},"add_component",[129,1137,1138],{"class":147},"(C_Health.",[129,1140,1124],{"class":905},[129,1142,1143],{"class":147},"())\n",[129,1145,1146,1148,1150,1153,1155,1157,1159,1161,1164,1166,1169,1171,1173],{"class":131,"line":442},[129,1147,1132],{"class":147},[129,1149,1135],{"class":905},[129,1151,1152],{"class":147},"(C_Velocity.",[129,1154,1124],{"class":905},[129,1156,243],{"class":147},[129,1158,1040],{"class":905},[129,1160,243],{"class":147},[129,1162,1163],{"class":163},"5",[129,1165,205],{"class":147},[129,1167,1168],{"class":163},"0",[129,1170,205],{"class":147},[129,1172,1168],{"class":163},[129,1174,1175],{"class":147},")))\n",[129,1177,1179],{"class":131,"line":1178},18,[129,1180,951],{"emptyLinePlaceholder":33},[129,1182,1183,1185,1188,1190,1192,1194,1196],{"class":131,"line":746},[129,1184,1111],{"class":135},[129,1186,1187],{"class":147}," target ",[129,1189,1092],{"class":135},[129,1191,1119],{"class":905},[129,1193,791],{"class":147},[129,1195,1124],{"class":905},[129,1197,1127],{"class":147},[129,1199,1200,1203,1205,1207,1209],{"class":131,"line":497},[129,1201,1202],{"class":147},"target.",[129,1204,1135],{"class":905},[129,1206,1138],{"class":147},[129,1208,1124],{"class":905},[129,1210,1143],{"class":147},[129,1212,1213,1215,1217,1219,1221,1223,1225,1227,1230,1232,1234,1236,1238,1240],{"class":131,"line":34},[129,1214,1202],{"class":147},[129,1216,1135],{"class":905},[129,1218,1152],{"class":147},[129,1220,1124],{"class":905},[129,1222,243],{"class":147},[129,1224,1040],{"class":905},[129,1226,243],{"class":147},[129,1228,1229],{"class":135},"-",[129,1231,1163],{"class":163},[129,1233,205],{"class":147},[129,1235,1168],{"class":163},[129,1237,205],{"class":147},[129,1239,1168],{"class":163},[129,1241,1175],{"class":147},[129,1243,1244],{"class":131,"line":468},[129,1245,951],{"emptyLinePlaceholder":33},[129,1247,1249],{"class":131,"line":1248},23,[129,1250,1251],{"class":945},"# Add entities to the world\n",[129,1253,1255,1258,1261,1264],{"class":131,"line":1254},24,[129,1256,1257],{"class":163},"ECS",[129,1259,1260],{"class":147},".world.",[129,1262,1263],{"class":905},"add_entity",[129,1265,1266],{"class":147},"(player)\n",[129,1268,1269,1271,1273,1275],{"class":131,"line":582},[129,1270,1257],{"class":163},[129,1272,1260],{"class":147},[129,1274,1263],{"class":905},[129,1276,1277],{"class":147},"(target)\n",[129,1279,1281],{"class":131,"line":1280},26,[129,1282,951],{"emptyLinePlaceholder":33},[129,1284,1286],{"class":131,"line":1285},27,[129,1287,1288],{"class":945},"# Add relationships between entities\n",[129,1290,1292,1294,1297,1299,1302,1304,1306,1309,1311],{"class":131,"line":1291},28,[129,1293,1132],{"class":147},[129,1295,1296],{"class":905},"add_relationship",[129,1298,243],{"class":147},[129,1300,1301],{"class":905},"Relationship",[129,1303,791],{"class":147},[129,1305,1124],{"class":905},[129,1307,1308],{"class":147},"(C_AllyTo.",[129,1310,1124],{"class":905},[129,1312,1313],{"class":147},"(), target))\n",[129,1315,1317],{"class":131,"line":1316},29,[129,1318,951],{"emptyLinePlaceholder":33},[129,1320,1322],{"class":131,"line":1321},30,[129,1323,1324],{"class":945},"# Systems define which entities to process\n",[129,1326,1328,1330,1333,1335],{"class":131,"line":1327},31,[129,1329,961],{"class":135},[129,1331,1332],{"class":905}," VelocitySystem",[129,1334,967],{"class":135},[129,1336,1337],{"class":905}," System\n",[129,1339,1341],{"class":131,"line":1340},32,[129,1342,951],{"emptyLinePlaceholder":33},[129,1344,1346,1348,1351,1354,1356,1359],{"class":131,"line":1345},33,[129,1347,1056],{"class":135},[129,1349,1350],{"class":905}," query",[129,1352,1353],{"class":147},"() ",[129,1355,1078],{"class":135},[129,1357,1358],{"class":905}," QueryBuilder",[129,1360,1084],{"class":147},[129,1362,1364,1367,1370,1373],{"class":131,"line":1363},34,[129,1365,1366],{"class":135},"    return",[129,1368,1369],{"class":147}," q.",[129,1371,1372],{"class":905},"with_all",[129,1374,1375],{"class":147},"([C_Velocity])\n",[129,1377,1379],{"class":131,"line":1378},35,[129,1380,951],{"emptyLinePlaceholder":33},[129,1382,1384,1386,1389,1392,1395,1398,1401,1404,1406,1409,1412,1414,1416,1418],{"class":131,"line":1383},36,[129,1385,1056],{"class":135},[129,1387,1388],{"class":905}," process",[129,1390,1391],{"class":147},"(entities: ",[129,1393,1394],{"class":905},"Array",[129,1396,1397],{"class":147},"[",[129,1399,1400],{"class":905},"Entity",[129,1402,1403],{"class":147},"], components: ",[129,1405,1394],{"class":905},[129,1407,1408],{"class":147},", delta: ",[129,1410,1411],{"class":905},"float",[129,1413,255],{"class":147},[129,1415,1078],{"class":135},[129,1417,1081],{"class":905},[129,1419,1084],{"class":147},[129,1421,1423,1426,1429,1432],{"class":131,"line":1422},37,[129,1424,1425],{"class":135},"    for",[129,1427,1428],{"class":147}," entity ",[129,1430,1431],{"class":135},"in",[129,1433,1434],{"class":147}," entities:\n",[129,1436,1438,1441,1444,1447,1450,1453,1456,1459],{"class":131,"line":1437},38,[129,1439,1440],{"class":135},"        var",[129,1442,1443],{"class":147}," vel ",[129,1445,1446],{"class":135},":=",[129,1448,1449],{"class":147}," entity.",[129,1451,1452],{"class":905},"get_component",[129,1454,1455],{"class":147},"(C_Velocity) ",[129,1457,1458],{"class":135},"as",[129,1460,1461],{"class":147}," C_Velocity\n",[129,1463,1465,1468,1471,1474,1477],{"class":131,"line":1464},39,[129,1466,1467],{"class":147},"        entity.position ",[129,1469,1470],{"class":135},"+=",[129,1472,1473],{"class":147}," vel.direction ",[129,1475,1476],{"class":135},"*",[129,1478,1479],{"class":147}," delta\n",[129,1481,1483],{"class":131,"line":1482},40,[129,1484,951],{"emptyLinePlaceholder":33},[129,1486,1488],{"class":131,"line":1487},41,[129,1489,1490],{"class":945},"# Register the system and start processing\n",[129,1492,1494,1496,1498,1501,1503,1506,1508,1510],{"class":131,"line":1493},42,[129,1495,1257],{"class":163},[129,1497,1260],{"class":147},[129,1499,1500],{"class":905},"add_system",[129,1502,243],{"class":147},[129,1504,1505],{"class":905},"VelocitySystem",[129,1507,791],{"class":147},[129,1509,1124],{"class":905},[129,1511,1143],{"class":147},[129,1513,1515],{"class":131,"line":1514},43,[129,1516,951],{"emptyLinePlaceholder":33},[129,1518,1520,1522,1525,1528,1530,1532,1534,1536],{"class":131,"line":1519},44,[129,1521,1056],{"class":135},[129,1523,1524],{"class":905}," _process",[129,1526,1527],{"class":147},"(delta: ",[129,1529,1411],{"class":905},[129,1531,255],{"class":147},[129,1533,1078],{"class":135},[129,1535,1081],{"class":905},[129,1537,1084],{"class":147},[129,1539,1541,1544,1546,1549],{"class":131,"line":1540},45,[129,1542,1543],{"class":163},"    ECS",[129,1545,791],{"class":147},[129,1547,1548],{"class":905},"process",[129,1550,1551],{"class":147},"(delta)\n",[363,1553,1555],{"id":1554},"quick-start-steps","Quick Start Steps",[1557,1558,1559,1568,1579],"ol",{},[800,1560,1561,1564,1565,1567],{},[370,1562,1563],{},"Install",": Download to ",[126,1566,885],{}," and enable in Project Settings",[800,1569,1570,1573,1574],{},[370,1571,1572],{},"Follow Guide",": ",[52,1575,1578],{"href":1576,"rel":1577},"https:\u002F\u002Fgithub.com\u002Fcsprance\u002Fgecs\u002Ftree\u002Fmain\u002Faddons\u002Fgecs\u002Fdocs\u002FGETTING_STARTED.md",[704],"Get your first ECS project running in 5 minutes →",[800,1580,1581,1573,1584],{},[370,1582,1583],{},"Learn More",[52,1585,1588],{"href":1586,"rel":1587},"https:\u002F\u002Fgithub.com\u002Fcsprance\u002Fgecs\u002Ftree\u002Fmain\u002Faddons\u002Fgecs\u002Fdocs\u002FCORE_CONCEPTS.md",[704],"Understand core ECS concepts →",[363,1590,1592],{"id":1591},"complete-documentation","Complete Documentation",[10,1594,1595],{},[370,1596,1597],{},"All documentation is located in the addon folder:",[10,1599,1600],{},[370,1601,1602,1603],{},"→ ",[52,1604,1607],{"href":1605,"rel":1606},"https:\u002F\u002Fgithub.com\u002Fcsprance\u002Fgecs\u002Ftree\u002Fmain\u002Faddons\u002Fgecs\u002FREADME.md",[704],"Complete Documentation Index",[115,1609,1611],{"id":1610},"quick-navigation","Quick Navigation",[797,1613,1614,1623,1632,1642],{},[800,1615,1616,1622],{},[370,1617,1618],{},[52,1619,1621],{"href":1576,"rel":1620},[704],"Getting Started"," - Build your first ECS project (5 min)",[800,1624,1625,1631],{},[370,1626,1627],{},[52,1628,1630],{"href":1586,"rel":1629},[704],"Core Concepts"," - Understand Entities, Components, Systems, Relationships (20 min)",[800,1633,1634,1641],{},[370,1635,1636],{},[52,1637,1640],{"href":1638,"rel":1639},"https:\u002F\u002Fgithub.com\u002Fcsprance\u002Fgecs\u002Ftree\u002Fmain\u002Faddons\u002Fgecs\u002Fdocs\u002FBEST_PRACTICES.md",[704],"Best Practices"," - Write maintainable ECS code (15 min)",[800,1643,1644,1651],{},[370,1645,1646],{},[52,1647,1650],{"href":1648,"rel":1649},"https:\u002F\u002Fgithub.com\u002Fcsprance\u002Fgecs\u002Ftree\u002Fmain\u002Faddons\u002Fgecs\u002Fdocs\u002FTROUBLESHOOTING.md",[704],"Troubleshooting"," - Solve common issues quickly",[115,1653,1655],{"id":1654},"advanced-features","Advanced Features",[797,1657,1658,1668,1677,1687],{},[800,1659,1660,1667],{},[370,1661,1662],{},[52,1663,1666],{"href":1664,"rel":1665},"https:\u002F\u002Fgithub.com\u002Fcsprance\u002Fgecs\u002Ftree\u002Fmain\u002Faddons\u002Fgecs\u002Fdocs\u002FCOMPONENT_QUERIES.md",[704],"Component Queries"," - Advanced property-based filtering",[800,1669,1670,1676],{},[370,1671,1672],{},[52,1673,790],{"href":1674,"rel":1675},"https:\u002F\u002Fgithub.com\u002Fcsprance\u002Fgecs\u002Ftree\u002Fmain\u002Faddons\u002Fgecs\u002Fdocs\u002FRELATIONSHIPS.md",[704]," - Entity linking and associations",[800,1678,1679,1686],{},[370,1680,1681],{},[52,1682,1685],{"href":1683,"rel":1684},"https:\u002F\u002Fgithub.com\u002Fcsprance\u002Fgecs\u002Ftree\u002Fmain\u002Faddons\u002Fgecs\u002Fdocs\u002FOBSERVERS.md",[704],"Observers"," - Reactive systems for component changes",[800,1688,1689,1696],{},[370,1690,1691],{},[52,1692,1695],{"href":1693,"rel":1694},"https:\u002F\u002Fgithub.com\u002Fcsprance\u002Fgecs\u002Ftree\u002Fmain\u002Faddons\u002Fgecs\u002Fdocs\u002FPERFORMANCE_OPTIMIZATION.md",[704],"Performance Optimization"," - Make your games run fast",[363,1698,1700],{"id":1699},"example-games","Example Games",[797,1702,1703,1713,1723],{},[800,1704,1705,1712],{},[370,1706,1707],{},[52,1708,1711],{"href":1709,"rel":1710},"https:\u002F\u002Fgithub.com\u002Fcsprance\u002Fgecs-101",[704],"GECS-101"," - A simple example",[800,1714,1715,1722],{},[370,1716,1717],{},[52,1718,1721],{"href":1719,"rel":1720},"https:\u002F\u002Fgithub.com\u002Fcsprance\u002Fgecs\u002Ftree\u002Fzombies-ate-my-neighbors\u002Fgame",[704],"Zombies Ate My Neighbors"," - Action arcade game",[800,1724,1725,1732],{},[370,1726,1727],{},[52,1728,1731],{"href":1729,"rel":1730},"https:\u002F\u002Fgithub.com\u002Fcsprance\u002Fgecs\u002Ftree\u002Fbreakout\u002Fgame",[704],"Breakout Clone"," - Classic brick breaker",[363,1734,1736],{"id":1735},"community","Community",[797,1738,1739,1749,1759],{},[800,1740,1741,1573,1744],{},[370,1742,1743],{},"Discord",[52,1745,1748],{"href":1746,"rel":1747},"https:\u002F\u002Fdiscord.gg\u002FeB43XU2tmn",[704],"Join our community",[800,1750,1751,1573,1754],{},[370,1752,1753],{},"Issues",[52,1755,1758],{"href":1756,"rel":1757},"https:\u002F\u002Fgithub.com\u002Fcsprance\u002Fgecs\u002Fissues",[704],"Report bugs or request features",[800,1760,1761,1573,1764],{},[370,1762,1763],{},"Discussions",[52,1765,1768],{"href":1766,"rel":1767},"https:\u002F\u002Fgithub.com\u002Fcsprance\u002Fgecs\u002Fdiscussions",[704],"Ask questions and share projects",[363,1770,1772],{"id":1771},"license","License",[10,1774,1775,1776,1781],{},"MIT - See ",[52,1777,1780],{"href":1778,"rel":1779},"https:\u002F\u002Fgithub.com\u002Fcsprance\u002Fgecs\u002Ftree\u002Fmain\u002FLICENSE",[704],"LICENSE"," for details.",[1783,1784],"hr",{},[10,1786,1787],{},[1788,1789,1790],"em",{},"GECS is provided as-is. If it breaks, you get to keep both pieces.",[10,1792,1793],{},[52,1794,1797],{"href":1795,"rel":1796},"https:\u002F\u002Fstar-history.com\u002F#csprance\u002Fgecs&Date",[704],[359,1798],{"alt":1799,"src":1800},"Star History Chart","https:\u002F\u002Fapi.star-history.com\u002Fsvg?repos=csprance\u002Fgecs&type=Date",[325,1802,1803],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}",{"title":20,"searchDepth":21,"depth":21,"links":1805},[1806,1807,1808,1809,1814,1815,1816,1820,1821,1822],{"id":760,"depth":21,"text":761},{"id":794,"depth":21,"text":795},{"id":856,"depth":21,"text":857},{"id":863,"depth":21,"text":864,"children":1810},[1811,1812,1813],{"id":867,"depth":151,"text":868},{"id":878,"depth":151,"text":879},{"id":892,"depth":151,"text":893},{"id":932,"depth":21,"text":933},{"id":1554,"depth":21,"text":1555},{"id":1591,"depth":21,"text":1592,"children":1817},[1818,1819],{"id":1610,"depth":151,"text":1611},{"id":1654,"depth":151,"text":1655},{"id":1699,"depth":21,"text":1700},{"id":1735,"depth":21,"text":1736},{"id":1771,"depth":21,"text":1772},"\u002Fimages\u002Fwork\u002Fgecs-logo.png",{"date":1825},"01\u002F01\u002F2026","\u002Fwork\u002Fgecs",{"title":755,"description":20},[1829,1830],"vscode","godot","work\u002Fgecs","XaCHtkDrqlxjeJK5Zx9X5RaLwxIu-48Y5I7UgsoeeU4",{"id":1834,"title":1835,"body":1836,"description":1840,"excerpt":23,"extension":24,"headerImage":1846,"images":1847,"meta":1853,"navigation":33,"order":1291,"password":23,"path":1854,"seo":1855,"size":132,"software":1856,"stem":1857,"__hash__":1858},"work\u002Fwork\u002Fghillie-suit.md","Ghillie Suit",{"type":7,"value":1837,"toc":1844},[1838,1841],[10,1839,1840],{},"The ghillie suit consists of many different uv strips groomed using a hair system in modo. This allows for each strand to be swapped out for a different type of material allowing for an almost infinite amount of variation.",[10,1842,1843],{},"The textures were created using Marvelous designer to simulate the cloth, modo to bake the simulated cloth and Substance Painter to create the textures.",{"title":20,"searchDepth":21,"depth":21,"links":1845},[],"\u002Fimages\u002Fuploads\u002F5d5445393f4bfghillie_suit_8c4cbc6ecf.png",[1848,1849,1850,1851,1852],"\u002Fimages\u002Fuploads\u002F5d54c448f2994ghillie_suit_dab_1_0a166e0370.png","\u002Fimages\u002Fuploads\u002F5d54c4492ade5ghillie_suit_dab_2_7d1a8c8a3e.png","\u002Fimages\u002Fuploads\u002F5d54c099b92c0ghillie_suit_dab_34dc79939a.png","\u002Fimages\u002Fuploads\u002F5d54c448c7966ghillia_suit_dab_3_96e9bda0a9.png","\u002Fimages\u002Fuploads\u002F5d54c44957a0aghillie_suit_dab_5_72ef01f20a.png",{"date":32},"\u002Fwork\u002Fghillie-suit",{"title":1835,"description":1840},[38,586,39,446],"work\u002Fghillie-suit","p45s0xEP0Jat4_cbawxUY3psAxGv6cX7NjKP-wvezDA",{"id":1860,"title":1861,"body":1862,"description":1866,"excerpt":23,"extension":24,"headerImage":1912,"images":1913,"meta":1914,"navigation":33,"order":1098,"password":23,"path":1916,"seo":1917,"size":21,"software":1918,"stem":1919,"__hash__":1920},"work\u002Fwork\u002Fgraphtoy-plus.md","GraphToy Plus",{"type":7,"value":1863,"toc":1910},[1864,1867,1876,1881,1901],[10,1865,1866],{},"A graphing tool built for graphics programmers — great for prototyping shader math, visualizing easing curves, and understanding how equations behave over time.",[10,1868,1869,1870,1875],{},"It's a fork of ",[52,1871,1874],{"href":1872,"rel":1873},"https:\u002F\u002Fgraphtoy.com",[704],"GraphToy"," by Inigo Quilez, fully rewritten from scratch in TypeScript and React, with a focus on making the tool more useful for real-time graphics work.",[10,1877,1878],{},[370,1879,1880],{},"What's New",[797,1882,1883,1889,1895],{},[800,1884,1885,1888],{},[370,1886,1887],{},"Syntax highlighting"," — equation input fields highlight as you type, making complex expressions much easier to read at a glance.",[800,1890,1891,1894],{},[370,1892,1893],{},"Dynamic variables (A–H)"," — use any letter A through H directly in your equations as live parameters. Each variable has configurable min\u002Fmax bounds and a draggable slider, so you can scrub through values in real time and see exactly how they affect your curve. Great for dialing in smoothstep ranges, tweaking falloff curves, or exploring parameter spaces interactively.",[800,1896,1897,1900],{},[370,1898,1899],{},"Color visualizer"," — the top-left preview shows the color produced by treating the first three formula outputs as RGB channels. Useful when you're building color ramps or blending functions and want immediate visual feedback beyond the graph lines.",[10,1902,1903],{},[370,1904,1905],{},[52,1906,1909],{"href":1907,"rel":1908},"https:\u002F\u002Fgraphtoy-plus.csprance.com",[704],"Try it live →",{"title":20,"searchDepth":21,"depth":21,"links":1911},[],"\u002Fimages\u002Fwork\u002Fgraphtoy-plus.png",[1912],{"date":1915},"04\u002F18\u002F2023","\u002Fwork\u002Fgraphtoy-plus",{"title":1861,"description":1866},[348],"work\u002Fgraphtoy-plus","ncyFq6YLRIKjE397Wf0o0k4qisH0ySgK1byxGXM-HHo",{"id":1922,"title":1923,"body":1924,"description":1928,"excerpt":23,"extension":24,"headerImage":1937,"images":1938,"meta":1945,"navigation":33,"order":1316,"password":23,"path":1946,"seo":1947,"size":132,"software":1948,"stem":1949,"__hash__":1950},"work\u002Fwork\u002Fhat.md","Hat",{"type":7,"value":1925,"toc":1935},[1926,1929,1932],[10,1927,1928],{},"Creating using modo. Textures created in Substance Painter.",[10,1930,1931],{},"Many different variations are created using a material only by tweaking the UV offset of a decal sub material in CRYENGINE.",[10,1933,1934],{},"Coloring\u002FCamo achieved via Dirt Layer using a tiling diffuse overlay texture.",{"title":20,"searchDepth":21,"depth":21,"links":1936},[],"\u002Fimages\u002Fuploads\u002F5d5444f47e0deflexcap_polycountfront_black_2048_4902dfeeb8.png",[1939,1940,1937,1941,1942,1943,1944],"\u002Fimages\u002Fuploads\u002F5d54451a766aeflexcap_crytekfront_camo3_2048_6bb85b6c59.png","\u002Fimages\u002Fuploads\u002F5d54451aa99c0flexcap_dopefishfront_black_2048_d4b9d7878a.png","\u002Fimages\u002Fuploads\u002F5d54451ae5d39flexcap_firefrontback_blue_2048_9526ba6e95.png","\u002Fimages\u002Fuploads\u002F5d54451acf14aflexcap_eilogo_khaki_200_75a0e2b5cf.png","\u002Fimages\u002Fuploads\u002F5d54451b4f3a8flexcap_usfrontback_blue_200_db780fbb31.png","\u002Fimages\u002Fuploads\u002F5d54451b23376flexcap_gbfront_camo4_2048_4e7999df12.png",{"date":32},"\u002Fwork\u002Fhat",{"title":1923,"description":1928},[38,39],"work\u002Fhat","xcZ9C9OtzltVFzy1BJrTdcLmClasiTNiv-bBk5l8mNk",{"id":1952,"title":55,"body":1953,"description":20,"excerpt":23,"extension":24,"headerImage":2003,"images":23,"meta":2004,"navigation":33,"order":151,"password":23,"path":54,"seo":2006,"size":132,"software":2007,"stem":2012,"__hash__":2013},"work\u002Fwork\u002Fheartland.md",{"type":7,"value":1954,"toc":1997},[1955,1959,1965,1969,1978,1982,1990,1994],[363,1956,1958],{"id":1957},"character-tools","Character & Tools",[10,1960,1961,1962,1964],{},"Contributed to The Division: Heartland as part of the character and tools team at Red Storm Entertainment. Created tools and assets for Snowdrop Engine spanning rigging, weighting, shaders like the ",[52,1963,505],{"href":553},", performance optimization, and scripting.",[363,1966,1968],{"id":1967},"procedural-world-systems","Procedural World Systems",[10,1970,1971,1972,1974,1975,1977],{},"Built several procedural systems used across the project in Houdini and Snowdrop, including ",[52,1973,591],{"href":679}," for rendering believable terrain at the horizon, and an ",[52,1976,45],{"href":71}," shader for holographic in-game UI.",[363,1979,1981],{"id":1980},"pipeline-tooling","Pipeline Tooling",[10,1983,1984,1985,1989],{},"Developed ",[52,1986,1988],{"href":1987},"\u002Fwork\u002Fsnoui","SnoUI",", a YAML-based Python UI framework that let artists and TDs build Snowdrop editor tools without writing verbose PySide boilerplate — dramatically speeding up internal tool development.",[363,1991,1993],{"id":1992},"outsourcing-support","Outsourcing Support",[10,1995,1996],{},"Supported outsourcing reviews and character asset integration, ensuring external deliverables met quality standards and integrated cleanly into the production pipeline.",{"title":20,"searchDepth":21,"depth":21,"links":1998},[1999,2000,2001,2002],{"id":1957,"depth":21,"text":1958},{"id":1967,"depth":21,"text":1968},{"id":1980,"depth":21,"text":1981},{"id":1992,"depth":21,"text":1993},"\u002Fimages\u002Fwork\u002Fheartland-logo.jpg",{"date":2005},"01\u002F01\u002F2022",{"title":55,"description":20},[75,347,74,2008,2009,2010,2011],"flow","maya","max","blender","work\u002Fheartland","6x4Tyx2ycGV1PKgXg_6Mq-JunxOSKJIulYRbyjhlmyA",{"id":2015,"title":103,"body":2016,"description":20,"excerpt":23,"extension":24,"headerImage":2122,"images":23,"meta":2123,"navigation":33,"order":167,"password":23,"path":102,"seo":2125,"size":132,"software":2126,"stem":2128,"__hash__":2129},"work\u002Fwork\u002Fmiscreated.md",{"type":7,"value":2017,"toc":2112},[2018,2022,2025,2029,2032,2036,2039,2043,2046,2050,2053,2079,2083,2088,2092,2105,2109],[363,2019,2021],{"id":2020},"shipped-on-steam","Shipped on Steam",[10,2023,2024],{},"Served as Lead Technical Artist and Art Lead at Entrada Interactive from 2012 to 2021, helping ship Miscreated on Steam. Helped design and implement many of the game's core systems while managing artists to ensure the team stayed productive and unblocked.",[363,2026,2028],{"id":2027},"game-assets","Game Assets",[10,2030,2031],{},"Created many of the game's assets including weapons, props, vehicles, vegetation, and creatures. Built the vast majority of item icons used in the UI, designing a system to automate the entire icon pipeline using CRYENGINE and Python's Pillow library.",[363,2033,2035],{"id":2034},"pipeline-tools","Pipeline & Tools",[10,2037,2038],{},"Developed tools for animators, level designers, and artists to solve common production problems and streamline workflows. This included automated LOD mesh creation, CRYENGINE export setup automation across multiple DCC tools (Maya, Modo, 3DS Max), and scripting in PyMel, Mel, MaxScript, and Modo's Python TDSDK.",[363,2040,2042],{"id":2041},"game-systems","Game Systems",[10,2044,2045],{},"Maintained and designed several core game systems including the loot spawn system, crafting recipes, a localization system supporting many languages, and the base-building system from an art perspective.",[363,2047,2049],{"id":2048},"rcon-server-tools","RCON & Server Tools",[10,2051,2052],{},"Created libraries and applications to interact with game servers using XMLRPC via RCON:",[797,2054,2055,2063,2071],{},[800,2056,2057,2062],{},[52,2058,2061],{"href":2059,"rel":2060},"https:\u002F\u002Fgithub.com\u002Fcsprance\u002Fnode-misrcon",[704],"node-misrcon"," — TypeScript library for RCON commands",[800,2064,2065,2070],{},[52,2066,2069],{"href":2067,"rel":2068},"https:\u002F\u002Fgithub.com\u002Fcsprance\u002FMisRCON",[704],"MisRCON"," — Electron\u002FReact\u002FTypeScript server management app, widely used by the community",[800,2072,2073,2078],{},[52,2074,2077],{"href":2075,"rel":2076},"https:\u002F\u002Fservers.miscreatedgame.com",[704],"Miscreated Server Browser"," — Web-based server browser",[363,2080,2082],{"id":2081},"atlas-map-visualization","Atlas — Map Visualization",[10,2084,1984,2085,2087],{},[52,2086,95],{"href":343},", a map visualization application to catalog and analyze the world's design and art assets. Used Python to parse game files and store metadata in SQLite, then built an Electron\u002FTypeScript viewer with Leaflet.js to display metadata on a 2D map of the game world.",[363,2089,2091],{"id":2090},"community-web","Community & Web",[10,2093,2094,2095,2100,2101,791],{},"Maintained the official community Discord and wrote bots to automate moderation and add community features. Helped create and maintain the company's social media presence and built the official websites for both ",[52,2096,2099],{"href":2097,"rel":2098},"https:\u002F\u002Fentradainteractive.com",[704],"Entrada Interactive"," and ",[52,2102,103],{"href":2103,"rel":2104},"https:\u002F\u002Fmiscreatedgame.com",[704],[363,2106,2108],{"id":2107},"steam-integration","Steam Integration",[10,2110,2111],{},"Maintained the Steam Inventory requirements to allow players to trade and sell items on the Steam Marketplace. Built web applications to track cheat reports and incoming game telemetry to help catch cheaters.",{"title":20,"searchDepth":21,"depth":21,"links":2113},[2114,2115,2116,2117,2118,2119,2120,2121],{"id":2020,"depth":21,"text":2021},{"id":2027,"depth":21,"text":2028},{"id":2034,"depth":21,"text":2035},{"id":2041,"depth":21,"text":2042},{"id":2048,"depth":21,"text":2049},{"id":2081,"depth":21,"text":2082},{"id":2090,"depth":21,"text":2091},{"id":2107,"depth":21,"text":2108},"\u002Fimages\u002Fwork\u002Fmiscreated-logo.jpg",{"date":2124},"01\u002F01\u002F2021",{"title":103,"description":20},[38,39,586,2127,348,347,346],"cryengine","work\u002Fmiscreated","QC6MfGuzBsxnYZNQFzIAynJI_jXkQqJtMMA3rma-qqo",{"id":2131,"title":2132,"body":2133,"description":2137,"excerpt":23,"extension":24,"headerImage":2146,"images":2147,"meta":2148,"navigation":33,"order":1280,"password":23,"path":2149,"seo":2150,"size":21,"software":2151,"stem":2152,"__hash__":2153},"work\u002Fwork\u002Fmobile-home.md","Mobile Home",{"type":7,"value":2134,"toc":2144},[2135,2138,2141],[10,2136,2137],{},"This mobile home prefab consists of a few tiling textures and an interior and an exterior model including all relevant proxies.",[10,2139,2140],{},"It is used in Miscreated in prefabs to allow for many different variations to be easily created and populated with props\u002Fspawners.",[10,2142,2143],{},"The glowing cones in the image represent a loot spawn spot.",{"title":20,"searchDepth":21,"depth":21,"links":2145},[],"\u002Fimages\u002Fuploads\u002F5d54c4cfd11d9mobile_home_2_e9f22383d7.png",[2146],{"date":32},"\u002Fwork\u002Fmobile-home",{"title":2132,"description":2137},[38,39],"work\u002Fmobile-home","iMOw3EFwimLuxpbB9zAG56YQSfrxe1pWOWS_LvluNiI",{"id":2155,"title":377,"body":2156,"description":20,"excerpt":23,"extension":24,"headerImage":2198,"images":23,"meta":2199,"navigation":33,"order":132,"password":23,"path":376,"seo":2200,"size":132,"software":2201,"stem":2204,"__hash__":2205},"work\u002Fwork\u002Frainbow-six-siege.md",{"type":7,"value":2157,"toc":2193},[2158,2162,2168,2172,2178,2182],[363,2159,2161],{"id":2160},"production-pipeline-migration","Production Pipeline Migration",[10,2163,2164,2165,2167],{},"Led an 8-month production pipeline migration from JIRA to ",[52,2166,382],{"href":381}," (ShotGrid) alongside Diana Fitzpatrick, with the goal of making ShotGrid the single source of truth for all game content production. This initiative coordinated 50+ contributors and transformed how thousands of people across the production track and deliver game content.",[363,2169,2171],{"id":2170},"shotgrid-integration-automation","ShotGrid Integration & Automation",[10,2173,2174,2175,2177],{},"Implemented the ShotGrid Event Daemon, JIRA bridge, custom Action Menu Items, and custom daemon plugins. Built and deployed external web services to process data and orchestrate communication between JIRA, ShotGrid, and internal servers, enabling seamless automated handoffs across the full content creation pipeline. See the full technical breakdown on the ",[52,2176,382],{"href":381}," page.",[363,2179,2181],{"id":2180},"recognition","Recognition",[10,2183,2184,2185,2188,2189,2192],{},"Awarded the ",[370,2186,2187],{},"Eye of the Storm"," ",[52,2190,2191],{"href":411},"award"," (2025) by Red Storm Entertainment \u002F Ubisoft for spearheading this initiative.",{"title":20,"searchDepth":21,"depth":21,"links":2194},[2195,2196,2197],{"id":2160,"depth":21,"text":2161},{"id":2170,"depth":21,"text":2171},{"id":2180,"depth":21,"text":2181},"\u002Fimages\u002Fwork\u002Fsiege-logo.png",{"date":1825},{"title":377,"description":20},[1829,348,347,2202,2008,2203,2010],"nodejs","anvil","work\u002Frainbow-six-siege","TLL5N-QRdwRjbhtxt2rarAs1a_xAYrqSl9r6hkTWcvE",{"id":2207,"title":2208,"body":2209,"description":2213,"excerpt":23,"extension":24,"headerImage":2219,"images":2220,"meta":2221,"navigation":33,"order":1248,"password":23,"path":2222,"seo":2223,"size":21,"software":2224,"stem":2225,"__hash__":2226},"work\u002Fwork\u002Froad-barriers.md","Road Barriers",{"type":7,"value":2210,"toc":2217},[2211,2214],[10,2212,2213],{},"These were created using modo and Substance Painter.",[10,2215,2216],{},"It is used in Miscreated as both a prop in the environment and a base part you can build with.",{"title":20,"searchDepth":21,"depth":21,"links":2218},[],"\u002Fimages\u002Fuploads\u002F5d54667b8461fjersey_hesco_header_1ba2ece5cf.png",[2219],{"date":32},"\u002Fwork\u002Froad-barriers",{"title":2208,"description":2213},[74,347],"work\u002Froad-barriers","uz4MB-OXbVWSl7mclxeAwsfQTxOO9zqQIDeQMy_FDVw",{"id":2228,"title":2229,"body":2230,"description":2234,"excerpt":23,"extension":24,"headerImage":2240,"images":2241,"meta":2242,"navigation":33,"order":1178,"password":23,"path":2243,"seo":2244,"size":132,"software":2245,"stem":2246,"__hash__":2247},"work\u002Fwork\u002Froad-tool-hda.md","Road Tool HDA",{"type":7,"value":2231,"toc":2238},[2232,2235],[10,2233,2234],{},"A Houdini Digital Asset for creating roads from splines. It includes many different paramters to choose from road width, center strips, side stripes and many more.",[10,2236,2237],{},"This is used in Unreal Engine using the Houdini Engine plugin.",{"title":20,"searchDepth":21,"depth":21,"links":2239},[],"\u002Fimages\u002Fuploads\u002F5d545cc7c07b6road_tool_header_3e69dc4bc0.png",[2240],{"date":32},"\u002Fwork\u002Froad-tool-hda",{"title":2229,"description":2234},[74,347,750],"work\u002Froad-tool-hda","WWrSPtu_qGUstVafcg1UJS8NJSNY4cL55zWV19Yj75w",{"id":2249,"title":2250,"body":2251,"description":2255,"excerpt":23,"extension":24,"headerImage":2264,"images":2265,"meta":2269,"navigation":33,"order":1254,"password":23,"path":2270,"seo":2271,"size":21,"software":2272,"stem":2273,"__hash__":2274},"work\u002Fwork\u002Fsee-dew.md","See-Dew",{"type":7,"value":2252,"toc":2262},[2253,2256,2259],[10,2254,2255],{},"This was modelled after the SeaDoo.",[10,2257,2258],{},"I took the Hi poly modified it to bake and converted it to low poly using modo.",[10,2260,2261],{},"Created and baked all textures using Substance Painter.",{"title":20,"searchDepth":21,"depth":21,"links":2263},[],"\u002Fimages\u002Fuploads\u002F5d8264162566eseedew_header_afdfae9cd2.jpg",[2266,2267,2268],"\u002Fimages\u002Fuploads\u002F5d8265fb83cfcseedew1_1d38ea85bc.png","\u002Fimages\u002Fuploads\u002F5d8265fd7d2f3seedew2_414d58bf02.png","\u002Fimages\u002Fuploads\u002F5d8265ff68eceseedew3_b529a98c94.png",{"date":32},"\u002Fwork\u002Fsee-dew",{"title":2250,"description":2255},[38,39],"work\u002Fsee-dew","MtZ-khmPYcIdBnTK1aeWfkW_p8R_RTC2kNgHtbPZdCc",{"id":2276,"title":2277,"body":2278,"description":2626,"excerpt":23,"extension":24,"headerImage":2604,"images":23,"meta":2627,"navigation":33,"order":1008,"password":23,"path":2629,"seo":2630,"size":21,"software":2631,"stem":2636,"__hash__":2637},"work\u002Fwork\u002Fseeker.md","Seeker",{"type":7,"value":2279,"toc":2620},[2280,2288,2292,2299,2307,2310,2314,2321,2514,2518,2521,2527,2531,2534,2591,2602,2605,2608,2611,2614,2617],[10,2281,2282,2283,791],{},"Seeker is an online asset browser and manager built for studio pipelines. I designed and built the entire backend, infrastructure, and DevOps — a Nuxt 3 frontend backed by an Express\u002FTsED API, Hasura GraphQL layer over PostgreSQL, and a full Docker Compose deployment system with CLI tooling for dev, staging, and production environments. UI design and a lot of the frontend were contributed by ",[52,2284,2287],{"href":2285,"rel":2286},"https:\u002F\u002Fjokermartini.com",[704],"John Martini",[363,2289,2291],{"id":2290},"ai-powered-discovery","AI-Powered Discovery",[10,2293,2294,2295,2298],{},"The standout feature is a hybrid semantic search system. When a user adds ",[126,2296,2297],{},"ai:true"," to their query, Seeker generates a vector embedding of their search terms, runs a similarity search against pre-computed embeddings of all asset names and descriptions, then sends only the top matches to an LLM for intelligent query expansion. The result is surfaced through the existing QueryFilter DSL — users get dramatically better results with no added UI complexity.",[120,2300,2305],{"className":2301,"code":2303,"language":2304},[2302],"language-text","# User types:\nfood ai:true\n\n# Vector search finds top matches:\n# - Donut (similarity: 0.89, tags: [\"bakery\", \"sweet\"])\n# - Cake  (similarity: 0.85, tags: [\"dessert\", \"celebration\"])\n# - Apple (similarity: 0.78, tags: [\"fruit\", \"healthy\"])\n\n# LLM expands to final query:\nfood +name:donut +name:cake +name:apple +tag:bakery +tag:dessert +tag:fruit\n","text",[126,2306,2303],{"__ignoreMap":20},[10,2308,2309],{},"ML tagging runs via Ollama (LLaVA) and automatically analyzes ingested assets to generate descriptive tags, making previously untagged assets discoverable immediately on ingest.",[363,2311,2313],{"id":2312},"data-connectors","Data Connectors",[10,2315,2316,2317,2320],{},"Seeker wraps around existing storage rather than requiring migration. Assets live where they already are — local filesystems, network drives, cloud storage — and Seeker indexes them through configurable Storage Points. A sync process (manual or automatic hourly) reads ",[126,2318,2319],{},"asset.json"," metadata files alongside each asset and ingests everything into the database.",[120,2322,2326],{"className":2323,"code":2324,"language":2325,"meta":20,"style":20},"language-json shiki shiki-themes github-light github-dark","{\n  \"name\": \"Donut\",\n  \"tags\": [\"round\", \"sprinkles\", \"food\"],\n  \"category\": [\"3D Assets\", \"Food\", \"Desserts\"],\n  \"collections\": [\"Party and Celebration\", \"Bakery Items\"],\n  \"description\": \"Classic sprinkle donut\",\n  \"creator\": \"Seeker Inc.\",\n  \"attributes\": {\n    \"color\": \"#FFF\",\n    \"flavor\": \"vanilla\",\n    \"width\": 5,\n    \"height\": 1.5\n  },\n  \"gallery\": [\"media\u002Fturntable.mp4\", \"media\u002Fbeauty.png\"]\n}\n","json",[126,2327,2328,2333,2346,2370,2392,2409,2421,2433,2441,2453,2465,2476,2486,2491,2509],{"__ignoreMap":20},[129,2329,2330],{"class":131,"line":132},[129,2331,2332],{"class":147},"{\n",[129,2334,2335,2338,2340,2343],{"class":131,"line":21},[129,2336,2337],{"class":163},"  \"name\"",[129,2339,1573],{"class":147},[129,2341,2342],{"class":179},"\"Donut\"",[129,2344,2345],{"class":147},",\n",[129,2347,2348,2351,2354,2357,2359,2362,2364,2367],{"class":131,"line":151},[129,2349,2350],{"class":163},"  \"tags\"",[129,2352,2353],{"class":147},": [",[129,2355,2356],{"class":179},"\"round\"",[129,2358,205],{"class":147},[129,2360,2361],{"class":179},"\"sprinkles\"",[129,2363,205],{"class":147},[129,2365,2366],{"class":179},"\"food\"",[129,2368,2369],{"class":147},"],\n",[129,2371,2372,2375,2377,2380,2382,2385,2387,2390],{"class":131,"line":167},[129,2373,2374],{"class":163},"  \"category\"",[129,2376,2353],{"class":147},[129,2378,2379],{"class":179},"\"3D Assets\"",[129,2381,205],{"class":147},[129,2383,2384],{"class":179},"\"Food\"",[129,2386,205],{"class":147},[129,2388,2389],{"class":179},"\"Desserts\"",[129,2391,2369],{"class":147},[129,2393,2394,2397,2399,2402,2404,2407],{"class":131,"line":70},[129,2395,2396],{"class":163},"  \"collections\"",[129,2398,2353],{"class":147},[129,2400,2401],{"class":179},"\"Party and Celebration\"",[129,2403,205],{"class":147},[129,2405,2406],{"class":179},"\"Bakery Items\"",[129,2408,2369],{"class":147},[129,2410,2411,2414,2416,2419],{"class":131,"line":410},[129,2412,2413],{"class":163},"  \"description\"",[129,2415,1573],{"class":147},[129,2417,2418],{"class":179},"\"Classic sprinkle donut\"",[129,2420,2345],{"class":147},[129,2422,2423,2426,2428,2431],{"class":131,"line":1008},[129,2424,2425],{"class":163},"  \"creator\"",[129,2427,1573],{"class":147},[129,2429,2430],{"class":179},"\"Seeker Inc.\"",[129,2432,2345],{"class":147},[129,2434,2435,2438],{"class":131,"line":678},[129,2436,2437],{"class":163},"  \"attributes\"",[129,2439,2440],{"class":147},": {\n",[129,2442,2443,2446,2448,2451],{"class":131,"line":1018},[129,2444,2445],{"class":163},"    \"color\"",[129,2447,1573],{"class":147},[129,2449,2450],{"class":179},"\"#FFF\"",[129,2452,2345],{"class":147},[129,2454,2455,2458,2460,2463],{"class":131,"line":1030},[129,2456,2457],{"class":163},"    \"flavor\"",[129,2459,1573],{"class":147},[129,2461,2462],{"class":179},"\"vanilla\"",[129,2464,2345],{"class":147},[129,2466,2467,2470,2472,2474],{"class":131,"line":1053},[129,2468,2469],{"class":163},"    \"width\"",[129,2471,1573],{"class":147},[129,2473,1163],{"class":163},[129,2475,2345],{"class":147},[129,2477,2478,2481,2483],{"class":131,"line":342},[129,2479,2480],{"class":163},"    \"height\"",[129,2482,1573],{"class":147},[129,2484,2485],{"class":163},"1.5\n",[129,2487,2488],{"class":131,"line":1098},[129,2489,2490],{"class":147},"  },\n",[129,2492,2493,2496,2498,2501,2503,2506],{"class":131,"line":551},[129,2494,2495],{"class":163},"  \"gallery\"",[129,2497,2353],{"class":147},[129,2499,2500],{"class":179},"\"media\u002Fturntable.mp4\"",[129,2502,205],{"class":147},[129,2504,2505],{"class":179},"\"media\u002Fbeauty.png\"",[129,2507,2508],{"class":147},"]\n",[129,2510,2511],{"class":131,"line":1108},[129,2512,2513],{"class":147},"}\n",[363,2515,2517],{"id":2516},"queryfilter-dsl","QueryFilter DSL",[10,2519,2520],{},"I built a custom query DSL that gives power users precise control over search. It supports name matching, tag and collection filters, attribute comparisons, AND\u002FOR logic, ordering, limits, distinct results, and archived asset visibility — all composable in a single search string.",[120,2522,2525],{"className":2523,"code":2524,"language":2304},[2302],"# Find medieval props that are either a weapon or armor, sorted by name\ncollection:Fantasy +attribute:type:_eq:weapon +attribute:type:_eq:armor orderby:asc:name\n\n# Find large assets colored red, or anything tagged \"sci-fi\"\nattribute:size:_gt:20 +attribute:color:_eq:#FF0000 +tag:sci-fi\n\n# Find unique creators across non-archived assets in two collections\ndistinct:creator +collection:Archives +collection:NewReleases archived:false\n\n# Regex name match with a tag filter and result limit\nname:_iregex:Shi* tag:vehicle limit:10\n",[126,2526,2524],{"__ignoreMap":20},[363,2528,2530],{"id":2529},"infrastructure-deployment","Infrastructure & Deployment",[10,2532,2533],{},"The full stack runs in Docker Compose with a custom CLI that handles dev resets, staging, production, and cleanup. Two deployment modes are supported out of the box: IP + port access for LAN deployments and Traefik + TLS for public domain deployments.",[120,2535,2539],{"className":2536,"code":2537,"language":2538,"meta":20,"style":20},"language-sh shiki shiki-themes github-light github-dark","pnpm run seeker:reset-dev    # tear down and rebuild dev environment\npnpm run seeker:start-prod   # start production stack (IP mode)\npnpm run seeker:start-staging # start staging stack (Traefik + TLS)\npnpm run seeker:clean        # prune unused Docker resources\n","sh",[126,2540,2541,2555,2567,2579],{"__ignoreMap":20},[129,2542,2543,2546,2549,2552],{"class":131,"line":132},[129,2544,2545],{"class":905},"pnpm",[129,2547,2548],{"class":179}," run",[129,2550,2551],{"class":179}," seeker:reset-dev",[129,2553,2554],{"class":945},"    # tear down and rebuild dev environment\n",[129,2556,2557,2559,2561,2564],{"class":131,"line":21},[129,2558,2545],{"class":905},[129,2560,2548],{"class":179},[129,2562,2563],{"class":179}," seeker:start-prod",[129,2565,2566],{"class":945},"   # start production stack (IP mode)\n",[129,2568,2569,2571,2573,2576],{"class":131,"line":151},[129,2570,2545],{"class":905},[129,2572,2548],{"class":179},[129,2574,2575],{"class":179}," seeker:start-staging",[129,2577,2578],{"class":945}," # start staging stack (Traefik + TLS)\n",[129,2580,2581,2583,2585,2588],{"class":131,"line":167},[129,2582,2545],{"class":905},[129,2584,2548],{"class":179},[129,2586,2587],{"class":179}," seeker:clean",[129,2589,2590],{"class":945},"        # prune unused Docker resources\n",[10,2592,2593,2594,2597,2598,2601],{},"Any push to ",[126,2595,2596],{},"main"," triggers an automated build and deploy via GitHub Actions, with a ",[126,2599,2600],{},"SKIP-DEPLOY"," commit message escape hatch and a production database reset flow.",[359,2603],{"src":2604},"\u002Fimages\u002Fwork\u002Fseeker-header.png",[359,2606],{"src":2607},"\u002Fimages\u002Fwork\u002Fseeker-browser.png",[359,2609],{"src":2610},"\u002Fimages\u002Fwork\u002Fseeker-asset-page.png",[359,2612],{"src":2613},"\u002Fimages\u002Fwork\u002Fseeker-users.png",[359,2615],{"src":2616},"\u002Fimages\u002Fwork\u002Fseeker-admin.png",[325,2618,2619],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":20,"searchDepth":21,"depth":21,"links":2621},[2622,2623,2624,2625],{"id":2290,"depth":21,"text":2291},{"id":2312,"depth":21,"text":2313},{"id":2516,"depth":21,"text":2517},{"id":2529,"depth":21,"text":2530},"Seeker is an online asset browser and manager built for studio pipelines. I designed and built the entire backend, infrastructure, and DevOps — a Nuxt 3 frontend backed by an Express\u002FTsED API, Hasura GraphQL layer over PostgreSQL, and a full Docker Compose deployment system with CLI tooling for dev, staging, and production environments. UI design and a lot of the frontend were contributed by John Martini.",{"date":2628},"01\u002F01\u002F2025","\u002Fwork\u002Fseeker",{"title":2277,"description":2626},[1829,2632,348,2633,2634,2635,2202],"nuxt","hasura","graphql","express","work\u002Fseeker","q3InBR0UngR3j-nlYNY2yLr2Dr5225thaMFA1TQNOVA",{"id":2639,"title":382,"body":2640,"description":2661,"excerpt":23,"extension":24,"headerImage":2662,"images":23,"meta":2663,"navigation":33,"order":1030,"password":552,"path":381,"seo":2664,"size":132,"software":2665,"stem":2666,"__hash__":2667},"work\u002Fwork\u002Fshotgrid.md",{"type":7,"value":2641,"toc":2659},[2642,2647,2650,2653,2656],[10,2643,2644,2646],{},[52,2645,377],{"href":376}," runs as a live service title with a relentless cadence of seasonal content drops, and by 2021 the production pipeline was buckling under its own complexity. The team was spread across a patchwork of tools — Jira, Excel, PowerPoint, Miro — with no unified source of truth. Jira in particular had become a bottleneck: its rigid, linear workflow couldn't accommodate the multi-department, non-linear nature of game art production at scale.",[10,2648,2649],{},"I co-led the studio's migration from Jira to Autodesk Flow Production Tracking (Shotgrid) alongside Senior Associate Producer Diana Benocilla. The goal was to replace the fragmented toolset with a single, flexible platform that could handle everything from asset tracking to art review to roadmapping — all in one place.",[10,2651,2652],{},"The core of the work was designing a schema that could scale with R6S's production complexity. We built a hierarchical data model — Asset → Task → Version → Notes — that gave every department a clear, navigable view of any asset's full lifecycle. With 14 departments integrated, from Concept Art and Tech Art through Narrative, Outsourcing, and Legal, the system had to be flexible enough to serve wildly different workflows without losing coherence.",[10,2654,2655],{},"On the automation side, we built a suite of tools using Shotgrid's Event Daemon and Python API — handling things like automatic task status flips on version approval, downstream task updates when upstream work was finalized, and data handoffs to the game engines (Anvil and Snowdrop). Webhooks connected Shotgrid outward to other production tools including Jira, Oasis, and the MTX pipeline.",[10,2657,2658],{},"The result was a system supporting over 500 active users across the full production organization, tracking 3,267 assets across 8 seasons of live content. We built 135 custom project pages and 41-step pipelines that replaced what had previously been a tangled \"bowl of noodles\" in Jira. Artists could finally see where their work stood, producers had real roadmapping data, and leadership had visibility across the entire content pipeline.",{"title":20,"searchDepth":21,"depth":21,"links":2660},[],"Rainbow Six Siege runs as a live service title with a relentless cadence of seasonal content drops, and by 2021 the production pipeline was buckling under its own complexity. The team was spread across a patchwork of tools — Jira, Excel, PowerPoint, Miro — with no unified source of truth. Jira in particular had become a bottleneck: its rigid, linear workflow couldn't accommodate the multi-department, non-linear nature of game art production at scale.","\u002Fimages\u002Fwork\u002Fsg-preview.png",{"date":2628},{"title":382,"description":2661},[1829,2632,348,2635,2202],"work\u002Fshotgrid","J3Y-fgPLHB3cDBM9EBkzXu8-vo0DR-bnU7DzqxzzbTQ",{"id":2669,"title":1988,"body":2670,"description":4661,"excerpt":23,"extension":24,"headerImage":4662,"images":23,"meta":4663,"navigation":33,"order":1018,"password":552,"path":1987,"seo":4665,"size":132,"software":4666,"stem":4667,"__hash__":4668},"work\u002Fwork\u002Fsnoui.md",{"type":7,"value":2671,"toc":4659},[2672,2682,2687,2690,2695,2710,2713,2718,2897,2902,2905,2911,4656],[10,2673,2674,2675,2677,2678,791],{},"A Python library for building tool UIs inside Snowdrop without writing verbose PySide boilerplate. Originally developed during ",[52,2676,55],{"href":54}," and used across multiple Snowdrop titles including ",[52,2679,2681],{"href":2680},"\u002Fwork\u002Fxdefiant","XDefiant",[10,2683,2684],{},[370,2685,2686],{},"The Problem",[10,2688,2689],{},"Most pipeline scripts need the same thing: a handful of inputs, a button, and something happens. But writing that in raw PySide means dozens of lines of widget instantiation, layout management, and signal wiring before you've written a single line of actual tool logic. The UI ends up overwhelming the code it exists to serve.",[10,2691,2692],{},[370,2693,2694],{},"The Solution",[10,2696,2697,2698,2701,2702,2705,2706,2709],{},"SnoUI flips the model. You describe your UI in a YAML string — just the fields you need, nothing else — and SnoUI builds it. State management follows a React-style ",[126,2699,2700],{},"get_state"," \u002F ",[126,2703,2704],{},"set_state"," pattern, and buttons are wired to tool functions with a single ",[126,2707,2708],{},"connect()"," call. The result is that your tool logic stays front and center, and the UI is just a short block of markup above it.",[10,2711,2712],{},"YAML was a deliberate choice: it's minimal, human-readable, supports arrays natively, and has almost no syntax to learn. The goal was zero barrier to adoption for pipeline TDs who just need to ship a tool.",[10,2714,2715],{},[370,2716,2717],{},"Define it, connect it, run it:",[120,2719,2723],{"className":2720,"code":2721,"language":2722,"meta":20,"style":20},"language-yaml shiki shiki-themes github-light github-dark","my_cool_radio:\n    type: radio\n    value: [A, B, See, Dee]\n    label: Radio Group\ndiv1:\n    type: divider\nlabel:\n    type: label\n    label: This is a label\nmy_cool_dropdown:\n    type: dropdown\n    label: Dropdown\n    value: ['Item1', 'Item2']\nmy_cool_button:\n    type: button\n    label: Joined To Next\n    join: True\n","yaml",[126,2724,2725,2733,2743,2770,2780,2787,2796,2803,2812,2821,2828,2837,2846,2862,2869,2878,2887],{"__ignoreMap":20},[129,2726,2727,2731],{"class":131,"line":132},[129,2728,2730],{"class":2729},"s9eBZ","my_cool_radio",[129,2732,1084],{"class":147},[129,2734,2735,2738,2740],{"class":131,"line":21},[129,2736,2737],{"class":2729},"    type",[129,2739,1573],{"class":147},[129,2741,2742],{"class":179},"radio\n",[129,2744,2745,2748,2750,2753,2755,2758,2760,2763,2765,2768],{"class":131,"line":151},[129,2746,2747],{"class":2729},"    value",[129,2749,2353],{"class":147},[129,2751,2752],{"class":179},"A",[129,2754,205],{"class":147},[129,2756,2757],{"class":179},"B",[129,2759,205],{"class":147},[129,2761,2762],{"class":179},"See",[129,2764,205],{"class":147},[129,2766,2767],{"class":179},"Dee",[129,2769,2508],{"class":147},[129,2771,2772,2775,2777],{"class":131,"line":167},[129,2773,2774],{"class":2729},"    label",[129,2776,1573],{"class":147},[129,2778,2779],{"class":179},"Radio Group\n",[129,2781,2782,2785],{"class":131,"line":70},[129,2783,2784],{"class":2729},"div1",[129,2786,1084],{"class":147},[129,2788,2789,2791,2793],{"class":131,"line":410},[129,2790,2737],{"class":2729},[129,2792,1573],{"class":147},[129,2794,2795],{"class":179},"divider\n",[129,2797,2798,2801],{"class":131,"line":1008},[129,2799,2800],{"class":2729},"label",[129,2802,1084],{"class":147},[129,2804,2805,2807,2809],{"class":131,"line":678},[129,2806,2737],{"class":2729},[129,2808,1573],{"class":147},[129,2810,2811],{"class":179},"label\n",[129,2813,2814,2816,2818],{"class":131,"line":1018},[129,2815,2774],{"class":2729},[129,2817,1573],{"class":147},[129,2819,2820],{"class":179},"This is a label\n",[129,2822,2823,2826],{"class":131,"line":1030},[129,2824,2825],{"class":2729},"my_cool_dropdown",[129,2827,1084],{"class":147},[129,2829,2830,2832,2834],{"class":131,"line":1053},[129,2831,2737],{"class":2729},[129,2833,1573],{"class":147},[129,2835,2836],{"class":179},"dropdown\n",[129,2838,2839,2841,2843],{"class":131,"line":342},[129,2840,2774],{"class":2729},[129,2842,1573],{"class":147},[129,2844,2845],{"class":179},"Dropdown\n",[129,2847,2848,2850,2852,2855,2857,2860],{"class":131,"line":1098},[129,2849,2747],{"class":2729},[129,2851,2353],{"class":147},[129,2853,2854],{"class":179},"'Item1'",[129,2856,205],{"class":147},[129,2858,2859],{"class":179},"'Item2'",[129,2861,2508],{"class":147},[129,2863,2864,2867],{"class":131,"line":551},[129,2865,2866],{"class":2729},"my_cool_button",[129,2868,1084],{"class":147},[129,2870,2871,2873,2875],{"class":131,"line":1108},[129,2872,2737],{"class":2729},[129,2874,1573],{"class":147},[129,2876,2877],{"class":179},"button\n",[129,2879,2880,2882,2884],{"class":131,"line":721},[129,2881,2774],{"class":2729},[129,2883,1573],{"class":147},[129,2885,2886],{"class":179},"Joined To Next\n",[129,2888,2889,2892,2894],{"class":131,"line":442},[129,2890,2891],{"class":2729},"    join",[129,2893,1573],{"class":147},[129,2895,2896],{"class":163},"True\n",[10,2898,2899],{},[370,2900,2901],{},"Full Example",[10,2903,2904],{},"The example below covers the full feature set — state management, visibility toggling, custom components, collapsible sections, and the debug pattern for testing tool logic without rendering the UI.",[10,2906,2907],{},[359,2908],{"alt":2909,"src":2910},"UI Example",".\u002Fimages\u002Fwork\u002Fexample-snoui.png",[120,2912,2916],{"className":2913,"code":2914,"language":2915,"meta":20,"style":20},"language-py shiki shiki-themes github-light github-dark","\"\"\"\nThis example demonstrates how to render all the UI elements available:\n\nWriting the UI separately from the tool code helps keep things about\nthe tool not the ui.\n\"\"\"\nimport random\nfrom datetime import datetime\nfrom random import randint, shuffle\nfrom typing import Optional\n\nfrom python3.code_generation.generated_classes import SRSX_FMWidgetTransferBox\nfrom python3.utilities.snoui import SnoUI, SnoUIComponent\nfrom python3.utilities.snoui.utils import label\nfrom python3.utilities.ui_utilities import create_collapsible_frame\nfrom sdvectormath import Matrix44\n\n\ndef my_cool_random_value_tool(ui: SnoUI, signal_name, context):\n    \"\"\"\n    This function is bound in the my_example_ui function with connect('my_cool_random_button', my_cool_random_button)\n    This button just causes the ui to set a bunch of random values to illustrate how to set state\n    Make sure to have ui be an argument in your function! The : SnoUI bit after the arg is a nice way to get\n    autocomplete for all the methods you can use with snoui\n    :param ui: The current SnoUI instance\n    :param signal_name: The name of the signal that triggered this function\n    :param context: A context Dict that is populated with some data like 'name' which is the name of the calling\n    component (What is defined in yml)\n    :return:\n    \"\"\"\n    # We can key things off the signal name\n    print(signal_name)\n    # Data about how this signal was called will exist in this dict\n    print(context)\n\n    # This field is parsed so it allows math expressions\n    ui.log(ui.get_state('my_cool_vec3s'))\n\n    # we can use data components to just store arbitrary data values\n    # here we just log the default then set it to something else\n    # and then log the changed value\n    ui.log(ui.get_state('my_data_component'))\n    ui.set_state('my_data_component', {'a dict key': 'some dict val'})\n    ui.log(ui.get_state('my_data_component'))\n\n    def randint_array(num):\n        return [randint(0, 100) for _ in range(num)]\n\n    def shuffle_and_return(_list):\n        _l = _list\n        shuffle(_l)\n        return _l\n\n    ui.set_state(\n        \"my_cool_mat44\",\n        Matrix44(\n            randint_array(4), randint_array(4), randint_array(4), randint_array(4)\n        ),\n    )\n    ui.set_state(\"my_cool_string\", \"Random Values\")\n    ui.set_state(\"my_cool_checkbox\", False)\n    ui.set_state(\"my_cool_vector3\", randint_array(3))\n    ui.set_state(\"my_cool_vector4\", randint_array(4))\n    ui.set_state(\"my_cool_vector2\", randint_array(2))\n    ui.set_state(\"my_cool_float\", randint(0, 100))\n    ui.set_state(\"my_cool_slider\", randint(0, 10))\n    ui.set_state(\n        \"my_cool_dropdown\", shuffle_and_return(ui.get_state(\"my_cool_dropdown\"))\n    )\n\n    # Sometimes we need to manually save the state to the database because it isn't called on any form events except\n    # when we explicitly get or set state through the middleware triggered by our tool function.\n    # Only works if you add persist_key='some-key'\n    # ui.persist()\n\n\ndef my_cool_tool(ui: SnoUI, signal_name):\n    \"\"\"\n    This is more of the tool code you're writing the UI for it is important, and we should focus our attention\n    here rather than writing verbose faceman\u002Fpyside code That's why it's usually good practice to put the business logic\n    up at the top of the file\n    \"\"\"\n    # Fetch a value from the ui\n    a_string_val = ui.get_state(\"my_cool_string\")\n    # If you have a log component in your ui these will show up there\n    ui.logger.add_error(a_string_val)\n    # We can also set the state of a ui component for things like clearing or defaulting values\n    ui.set_state(\n        \"my_cool_string\", f\"State Set: {datetime.now()}! Signal Name: {signal_name}\"\n    )\n    ui.set_state('my_progress', random.random())\n    ui.set_state(\n        'my_nested_comp_a', f\"State Set: {datetime.now()}! Signal Name: {signal_name}\"\n    )\n    ui.set_state('my_nested_comp_b', random.random())\n    # Log a value\n    ui.log(ui.get_state(\"my_cool_mat44\"))\n    ui.log(ui.get_state(\"my_nested_comp_a\"))\n\n\ndef handle_ui_update(ui: SnoUI, signal_name, context):\n    \"\"\"\n    We can handle visibility toggles here based on UI state\n\n    :param ui:\n    :param signal_name:\n    :param context:\n    :return:\n    \"\"\"\n    # We can create little one off state objects like this to use just like react.useState(initialState)\n    # use_state returns a tuple of (value, setter)\n    [viz_a, set_viz_a] = ui.use_state(True)\n    [viz_b, set_viz_b] = ui.use_state(False)\n\n    # Check the name of the component that triggered this signal to be emitted\n    # This lets you wrap multiple logic paths in one function\n    if context.get('name') == 'my_cool_viz_toggle_button':\n        ui.set_visible('my_cool_vec3s', set_viz_a(not viz_a))\n\n    if context.get('name') == 'my_cool_viz_toggle_button2':\n        ui.set_visible('my_cool_string_2', set_viz_b(not viz_b))\n\n\nclass CustomComponent(SnoUIComponent):\n    \"\"\"\n    We can use a custom component for times when we need something more advanced.\n    Just create it wrap it in a hbox or vbox and use it by name in the yaml below\n    \"\"\"\n\n    # You always need to override build() at least.\n    def build(self) -> SRSX_FMWidgetTransferBox:\n        # Create the faceman component\n        contents = []\n        t = label(self.kwargs.get(\"value\"))\n        contents.append(t)\n        # Add the widget to the widget dict this is used for visibility\n        self.widgets['label'] = t.get_widget()\n\n        # Can also set it as an attribute on the class\n        self.label_widget = self.widgets['label']\n\n        collapsible_t, w = create_collapsible_frame(contents, self.kwargs.get(\"label\"))\n        # return the TransferBox or wrapper transfer box\n        return collapsible_t\n\n    # if we'd like to get and set state we also need to define those with the correct args\n    def get_state(self) -> Optional[bool]:\n        # we can use the attribute here we set\n        return self.label_widget.get_text()\n\n    def set_state(self, state: bool) -> None:\n        # or use the widgets dict\n        self.widgets['label'].set_text(state)\n\n\ndef my_example_ui():\n    \"\"\"\n    This is the main function we run that builds and shows the ui\n    We define how our ui looks in yaml\n    Then connect our tool code functions\n    \"\"\"\n    # This is the actual UI layout yml code that describes our entire ui\n    #  This could also be a python dict\n    ui_text = \"\"\"\nmy_cool_radio:\n    type: radio\n    value: [A, B, See, Dee]\n    label: Radio Group\nmy_cool_mat44:\n    type: matrix44\n    label: My Cool Matrix44\n    value: [[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]]\nmy_cool_string:\n    type: string\n    label: My Cool String\n    value: Default Value\nmy_cool_checkbox:\n    type: checkbox\n    label: My Cool Checkbox\n    value: false\ndiv2:\n    type: divider\nmy_cool_vector3:\n    type: vector3\n    label: My Cool Vec3\n    value: [1.5, 0, 1]\nmy_cool_vector4:\n    type: vector4\n    label: My Cool Vec4\n    value: [1.5, 0, 1, 1]\nmy_cool_vector2:\n    type: vector2\n    label: My Cool Vec2\n    value: [1.5, 0]\nmy_cool_float:\n    type: float\n    label: My Cool Float\n    value: 1.5\nmy_cool_label:\n    type: label\n    label: Label For Slider Joined \n    join: True\nmy_cool_slider:\n    type: slider\n    min: 0\n    max: 10\n    step: .5\n    value: 5\ndiv1:\n    type: divider\nlabel:\n    type: label\n    label: This is a label\nmy_cool_dropdown:\n    type: dropdown\n    label: Dropdown\n    value: ['Item1', 'Item2']\nmy_cool_button:\n    type: button\n    label: Joined To Next\n    join: True\nmy_cool_random_button:\n    type: button\n    label: Random Values\n    join: True\nmy_cool_viz_toggle_button:\n    type: button\n    label: I Toggle Visibility\n    join: true\nmy_cool_viz_toggle_button2:\n    type: button\n    label: I Also Toggle Visibility\nmy_cool_vec3s:\n    type: vector3s\n    label: Vector3 String\n    value: [a, b, c]\nmy_cool_string_2:\n    type: string\n    label: A String That Shows up\n    value: I just showed up!\n    visible: False\nmy_progress:\n    type: progress\nmy_custom_component:\n    type: my_custom_component\n    label: Custom Component\n    value: [1,2,3]\nmy_data_component:\n    type: data\n    value: whatever you want\nmy_collapsible:\n    type: collapsible\n    label: Collapsible 1\n    children:\n        my_nested_comp_a:\n            type: string\n            label: String Component A\n            value: A String Component A\n        my_nested_comp_b:\n            type: float\n            label: Float Component B\n            value: 1.0\nmy_collapsible2:\n    type: collapsible\n    label: Collapsible 2\n    children:\n        my_nested_comp_c:\n            type: string\n            label: String Component C\n            value: A String Component C\n        my_nested_comp_d:\n            type: float\n            label: Float Component D\n            value: 1.0\n\"\"\"\n    # First we instantiate the ui\n    ui = SnoUI(\n        \"My Cool Script Title\",\n        ui_text,\n        log=True,  # adding log=True is usually a good idea\n        # Add in a dict with the type of the component and the Class Reference to add custom components\n        components={'my_custom_component': CustomComponent},\n        # persist_key=\"example-snoui:2.0.4\",  # Optionally enable persistence with a unique persist key\n    )\n\n    # Then each button can be 'connected' like this by name to a function\n    # When the function is connected it will have the ui instance above passed in to it for you\n    ui.connect(\"my_cool_button\", my_cool_tool)\n    ui.connect(\"my_cool_random_button\", my_cool_random_value_tool)\n    ui.connect(\"my_cool_viz_toggle_button\", handle_ui_update)\n    ui.connect(\"my_cool_viz_toggle_button2\", handle_ui_update)\n\n    # Finally show the UI\n    ui.render()  # Comment me out to debug\n\n    # DEBUG HOW TO\n    #  If we are working on a script and we need to run it using the same inputs to test, it is much easier to\n    #  Comment out the ui.render() function above then explicitly set the state manually and then\n    #  call the tool code function needed passing in the SnoUI instance\n\n    # Uncomment below to debug\n    # ui.set_state('my_cool_vector4', [1, 2, 3, 4])\n    # ui.set_state('my_cool_vector2', [1, 2])\n    # my_cool_tool(ui)\n\n\n# To kick the whole thing off we run the function that calls ui.render()\nmy_example_ui()\n","py",[126,2917,2918,2923,2928,2932,2937,2942,2946,2951,2956,2961,2966,2970,2975,2980,2985,2990,2995,2999,3003,3008,3013,3018,3023,3028,3033,3038,3043,3048,3053,3058,3062,3067,3072,3077,3082,3086,3091,3096,3100,3105,3110,3115,3120,3125,3129,3133,3139,3145,3150,3156,3162,3168,3174,3179,3185,3191,3197,3203,3209,3215,3221,3227,3233,3239,3245,3251,3257,3262,3268,3273,3278,3284,3290,3296,3302,3307,3312,3318,3323,3329,3335,3341,3346,3352,3358,3364,3370,3376,3381,3387,3392,3398,3403,3409,3414,3420,3426,3432,3438,3443,3448,3454,3459,3465,3470,3476,3482,3488,3493,3498,3504,3510,3516,3522,3527,3533,3539,3545,3551,3556,3562,3568,3573,3578,3584,3589,3595,3601,3606,3611,3617,3623,3629,3635,3641,3647,3653,3659,3664,3670,3676,3681,3687,3693,3699,3704,3710,3716,3722,3728,3733,3739,3745,3751,3756,3761,3767,3772,3778,3784,3790,3795,3801,3807,3813,3819,3825,3831,3837,3843,3849,3855,3861,3867,3873,3879,3885,3891,3897,3903,3909,3915,3921,3927,3933,3939,3945,3951,3957,3963,3969,3975,3981,3987,3993,3999,4005,4011,4017,4023,4029,4035,4041,4047,4053,4059,4065,4071,4077,4083,4088,4094,4099,4105,4111,4117,4123,4129,4135,4141,4147,4152,4158,4163,4169,4174,4180,4185,4191,4197,4203,4208,4214,4220,4226,4232,4238,4244,4249,4255,4261,4267,4273,4279,4285,4291,4297,4303,4309,4315,4321,4327,4333,4339,4345,4351,4357,4363,4369,4375,4381,4387,4393,4399,4404,4410,4415,4421,4426,4432,4438,4444,4449,4455,4460,4465,4471,4477,4483,4489,4495,4501,4507,4513,4518,4523,4529,4535,4541,4547,4553,4559,4564,4570,4576,4581,4587,4593,4599,4605,4610,4616,4622,4628,4634,4639,4644,4650],{"__ignoreMap":20},[129,2919,2920],{"class":131,"line":132},[129,2921,2922],{},"\"\"\"\n",[129,2924,2925],{"class":131,"line":21},[129,2926,2927],{},"This example demonstrates how to render all the UI elements available:\n",[129,2929,2930],{"class":131,"line":151},[129,2931,951],{"emptyLinePlaceholder":33},[129,2933,2934],{"class":131,"line":167},[129,2935,2936],{},"Writing the UI separately from the tool code helps keep things about\n",[129,2938,2939],{"class":131,"line":70},[129,2940,2941],{},"the tool not the ui.\n",[129,2943,2944],{"class":131,"line":410},[129,2945,2922],{},[129,2947,2948],{"class":131,"line":1008},[129,2949,2950],{},"import random\n",[129,2952,2953],{"class":131,"line":678},[129,2954,2955],{},"from datetime import datetime\n",[129,2957,2958],{"class":131,"line":1018},[129,2959,2960],{},"from random import randint, shuffle\n",[129,2962,2963],{"class":131,"line":1030},[129,2964,2965],{},"from typing import Optional\n",[129,2967,2968],{"class":131,"line":1053},[129,2969,951],{"emptyLinePlaceholder":33},[129,2971,2972],{"class":131,"line":342},[129,2973,2974],{},"from python3.code_generation.generated_classes import SRSX_FMWidgetTransferBox\n",[129,2976,2977],{"class":131,"line":1098},[129,2978,2979],{},"from python3.utilities.snoui import SnoUI, SnoUIComponent\n",[129,2981,2982],{"class":131,"line":551},[129,2983,2984],{},"from python3.utilities.snoui.utils import label\n",[129,2986,2987],{"class":131,"line":1108},[129,2988,2989],{},"from python3.utilities.ui_utilities import create_collapsible_frame\n",[129,2991,2992],{"class":131,"line":721},[129,2993,2994],{},"from sdvectormath import Matrix44\n",[129,2996,2997],{"class":131,"line":442},[129,2998,951],{"emptyLinePlaceholder":33},[129,3000,3001],{"class":131,"line":1178},[129,3002,951],{"emptyLinePlaceholder":33},[129,3004,3005],{"class":131,"line":746},[129,3006,3007],{},"def my_cool_random_value_tool(ui: SnoUI, signal_name, context):\n",[129,3009,3010],{"class":131,"line":497},[129,3011,3012],{},"    \"\"\"\n",[129,3014,3015],{"class":131,"line":34},[129,3016,3017],{},"    This function is bound in the my_example_ui function with connect('my_cool_random_button', my_cool_random_button)\n",[129,3019,3020],{"class":131,"line":468},[129,3021,3022],{},"    This button just causes the ui to set a bunch of random values to illustrate how to set state\n",[129,3024,3025],{"class":131,"line":1248},[129,3026,3027],{},"    Make sure to have ui be an argument in your function! The : SnoUI bit after the arg is a nice way to get\n",[129,3029,3030],{"class":131,"line":1254},[129,3031,3032],{},"    autocomplete for all the methods you can use with snoui\n",[129,3034,3035],{"class":131,"line":582},[129,3036,3037],{},"    :param ui: The current SnoUI instance\n",[129,3039,3040],{"class":131,"line":1280},[129,3041,3042],{},"    :param signal_name: The name of the signal that triggered this function\n",[129,3044,3045],{"class":131,"line":1285},[129,3046,3047],{},"    :param context: A context Dict that is populated with some data like 'name' which is the name of the calling\n",[129,3049,3050],{"class":131,"line":1291},[129,3051,3052],{},"    component (What is defined in yml)\n",[129,3054,3055],{"class":131,"line":1316},[129,3056,3057],{},"    :return:\n",[129,3059,3060],{"class":131,"line":1321},[129,3061,3012],{},[129,3063,3064],{"class":131,"line":1327},[129,3065,3066],{},"    # We can key things off the signal name\n",[129,3068,3069],{"class":131,"line":1340},[129,3070,3071],{},"    print(signal_name)\n",[129,3073,3074],{"class":131,"line":1345},[129,3075,3076],{},"    # Data about how this signal was called will exist in this dict\n",[129,3078,3079],{"class":131,"line":1363},[129,3080,3081],{},"    print(context)\n",[129,3083,3084],{"class":131,"line":1378},[129,3085,951],{"emptyLinePlaceholder":33},[129,3087,3088],{"class":131,"line":1383},[129,3089,3090],{},"    # This field is parsed so it allows math expressions\n",[129,3092,3093],{"class":131,"line":1422},[129,3094,3095],{},"    ui.log(ui.get_state('my_cool_vec3s'))\n",[129,3097,3098],{"class":131,"line":1437},[129,3099,951],{"emptyLinePlaceholder":33},[129,3101,3102],{"class":131,"line":1464},[129,3103,3104],{},"    # we can use data components to just store arbitrary data values\n",[129,3106,3107],{"class":131,"line":1482},[129,3108,3109],{},"    # here we just log the default then set it to something else\n",[129,3111,3112],{"class":131,"line":1487},[129,3113,3114],{},"    # and then log the changed value\n",[129,3116,3117],{"class":131,"line":1493},[129,3118,3119],{},"    ui.log(ui.get_state('my_data_component'))\n",[129,3121,3122],{"class":131,"line":1514},[129,3123,3124],{},"    ui.set_state('my_data_component', {'a dict key': 'some dict val'})\n",[129,3126,3127],{"class":131,"line":1519},[129,3128,3119],{},[129,3130,3131],{"class":131,"line":1540},[129,3132,951],{"emptyLinePlaceholder":33},[129,3134,3136],{"class":131,"line":3135},46,[129,3137,3138],{},"    def randint_array(num):\n",[129,3140,3142],{"class":131,"line":3141},47,[129,3143,3144],{},"        return [randint(0, 100) for _ in range(num)]\n",[129,3146,3148],{"class":131,"line":3147},48,[129,3149,951],{"emptyLinePlaceholder":33},[129,3151,3153],{"class":131,"line":3152},49,[129,3154,3155],{},"    def shuffle_and_return(_list):\n",[129,3157,3159],{"class":131,"line":3158},50,[129,3160,3161],{},"        _l = _list\n",[129,3163,3165],{"class":131,"line":3164},51,[129,3166,3167],{},"        shuffle(_l)\n",[129,3169,3171],{"class":131,"line":3170},52,[129,3172,3173],{},"        return _l\n",[129,3175,3177],{"class":131,"line":3176},53,[129,3178,951],{"emptyLinePlaceholder":33},[129,3180,3182],{"class":131,"line":3181},54,[129,3183,3184],{},"    ui.set_state(\n",[129,3186,3188],{"class":131,"line":3187},55,[129,3189,3190],{},"        \"my_cool_mat44\",\n",[129,3192,3194],{"class":131,"line":3193},56,[129,3195,3196],{},"        Matrix44(\n",[129,3198,3200],{"class":131,"line":3199},57,[129,3201,3202],{},"            randint_array(4), randint_array(4), randint_array(4), randint_array(4)\n",[129,3204,3206],{"class":131,"line":3205},58,[129,3207,3208],{},"        ),\n",[129,3210,3212],{"class":131,"line":3211},59,[129,3213,3214],{},"    )\n",[129,3216,3218],{"class":131,"line":3217},60,[129,3219,3220],{},"    ui.set_state(\"my_cool_string\", \"Random Values\")\n",[129,3222,3224],{"class":131,"line":3223},61,[129,3225,3226],{},"    ui.set_state(\"my_cool_checkbox\", False)\n",[129,3228,3230],{"class":131,"line":3229},62,[129,3231,3232],{},"    ui.set_state(\"my_cool_vector3\", randint_array(3))\n",[129,3234,3236],{"class":131,"line":3235},63,[129,3237,3238],{},"    ui.set_state(\"my_cool_vector4\", randint_array(4))\n",[129,3240,3242],{"class":131,"line":3241},64,[129,3243,3244],{},"    ui.set_state(\"my_cool_vector2\", randint_array(2))\n",[129,3246,3248],{"class":131,"line":3247},65,[129,3249,3250],{},"    ui.set_state(\"my_cool_float\", randint(0, 100))\n",[129,3252,3254],{"class":131,"line":3253},66,[129,3255,3256],{},"    ui.set_state(\"my_cool_slider\", randint(0, 10))\n",[129,3258,3260],{"class":131,"line":3259},67,[129,3261,3184],{},[129,3263,3265],{"class":131,"line":3264},68,[129,3266,3267],{},"        \"my_cool_dropdown\", shuffle_and_return(ui.get_state(\"my_cool_dropdown\"))\n",[129,3269,3271],{"class":131,"line":3270},69,[129,3272,3214],{},[129,3274,3276],{"class":131,"line":3275},70,[129,3277,951],{"emptyLinePlaceholder":33},[129,3279,3281],{"class":131,"line":3280},71,[129,3282,3283],{},"    # Sometimes we need to manually save the state to the database because it isn't called on any form events except\n",[129,3285,3287],{"class":131,"line":3286},72,[129,3288,3289],{},"    # when we explicitly get or set state through the middleware triggered by our tool function.\n",[129,3291,3293],{"class":131,"line":3292},73,[129,3294,3295],{},"    # Only works if you add persist_key='some-key'\n",[129,3297,3299],{"class":131,"line":3298},74,[129,3300,3301],{},"    # ui.persist()\n",[129,3303,3305],{"class":131,"line":3304},75,[129,3306,951],{"emptyLinePlaceholder":33},[129,3308,3310],{"class":131,"line":3309},76,[129,3311,951],{"emptyLinePlaceholder":33},[129,3313,3315],{"class":131,"line":3314},77,[129,3316,3317],{},"def my_cool_tool(ui: SnoUI, signal_name):\n",[129,3319,3321],{"class":131,"line":3320},78,[129,3322,3012],{},[129,3324,3326],{"class":131,"line":3325},79,[129,3327,3328],{},"    This is more of the tool code you're writing the UI for it is important, and we should focus our attention\n",[129,3330,3332],{"class":131,"line":3331},80,[129,3333,3334],{},"    here rather than writing verbose faceman\u002Fpyside code That's why it's usually good practice to put the business logic\n",[129,3336,3338],{"class":131,"line":3337},81,[129,3339,3340],{},"    up at the top of the file\n",[129,3342,3344],{"class":131,"line":3343},82,[129,3345,3012],{},[129,3347,3349],{"class":131,"line":3348},83,[129,3350,3351],{},"    # Fetch a value from the ui\n",[129,3353,3355],{"class":131,"line":3354},84,[129,3356,3357],{},"    a_string_val = ui.get_state(\"my_cool_string\")\n",[129,3359,3361],{"class":131,"line":3360},85,[129,3362,3363],{},"    # If you have a log component in your ui these will show up there\n",[129,3365,3367],{"class":131,"line":3366},86,[129,3368,3369],{},"    ui.logger.add_error(a_string_val)\n",[129,3371,3373],{"class":131,"line":3372},87,[129,3374,3375],{},"    # We can also set the state of a ui component for things like clearing or defaulting values\n",[129,3377,3379],{"class":131,"line":3378},88,[129,3380,3184],{},[129,3382,3384],{"class":131,"line":3383},89,[129,3385,3386],{},"        \"my_cool_string\", f\"State Set: {datetime.now()}! Signal Name: {signal_name}\"\n",[129,3388,3390],{"class":131,"line":3389},90,[129,3391,3214],{},[129,3393,3395],{"class":131,"line":3394},91,[129,3396,3397],{},"    ui.set_state('my_progress', random.random())\n",[129,3399,3401],{"class":131,"line":3400},92,[129,3402,3184],{},[129,3404,3406],{"class":131,"line":3405},93,[129,3407,3408],{},"        'my_nested_comp_a', f\"State Set: {datetime.now()}! Signal Name: {signal_name}\"\n",[129,3410,3412],{"class":131,"line":3411},94,[129,3413,3214],{},[129,3415,3417],{"class":131,"line":3416},95,[129,3418,3419],{},"    ui.set_state('my_nested_comp_b', random.random())\n",[129,3421,3423],{"class":131,"line":3422},96,[129,3424,3425],{},"    # Log a value\n",[129,3427,3429],{"class":131,"line":3428},97,[129,3430,3431],{},"    ui.log(ui.get_state(\"my_cool_mat44\"))\n",[129,3433,3435],{"class":131,"line":3434},98,[129,3436,3437],{},"    ui.log(ui.get_state(\"my_nested_comp_a\"))\n",[129,3439,3441],{"class":131,"line":3440},99,[129,3442,951],{"emptyLinePlaceholder":33},[129,3444,3446],{"class":131,"line":3445},100,[129,3447,951],{"emptyLinePlaceholder":33},[129,3449,3451],{"class":131,"line":3450},101,[129,3452,3453],{},"def handle_ui_update(ui: SnoUI, signal_name, context):\n",[129,3455,3457],{"class":131,"line":3456},102,[129,3458,3012],{},[129,3460,3462],{"class":131,"line":3461},103,[129,3463,3464],{},"    We can handle visibility toggles here based on UI state\n",[129,3466,3468],{"class":131,"line":3467},104,[129,3469,951],{"emptyLinePlaceholder":33},[129,3471,3473],{"class":131,"line":3472},105,[129,3474,3475],{},"    :param ui:\n",[129,3477,3479],{"class":131,"line":3478},106,[129,3480,3481],{},"    :param signal_name:\n",[129,3483,3485],{"class":131,"line":3484},107,[129,3486,3487],{},"    :param context:\n",[129,3489,3491],{"class":131,"line":3490},108,[129,3492,3057],{},[129,3494,3496],{"class":131,"line":3495},109,[129,3497,3012],{},[129,3499,3501],{"class":131,"line":3500},110,[129,3502,3503],{},"    # We can create little one off state objects like this to use just like react.useState(initialState)\n",[129,3505,3507],{"class":131,"line":3506},111,[129,3508,3509],{},"    # use_state returns a tuple of (value, setter)\n",[129,3511,3513],{"class":131,"line":3512},112,[129,3514,3515],{},"    [viz_a, set_viz_a] = ui.use_state(True)\n",[129,3517,3519],{"class":131,"line":3518},113,[129,3520,3521],{},"    [viz_b, set_viz_b] = ui.use_state(False)\n",[129,3523,3525],{"class":131,"line":3524},114,[129,3526,951],{"emptyLinePlaceholder":33},[129,3528,3530],{"class":131,"line":3529},115,[129,3531,3532],{},"    # Check the name of the component that triggered this signal to be emitted\n",[129,3534,3536],{"class":131,"line":3535},116,[129,3537,3538],{},"    # This lets you wrap multiple logic paths in one function\n",[129,3540,3542],{"class":131,"line":3541},117,[129,3543,3544],{},"    if context.get('name') == 'my_cool_viz_toggle_button':\n",[129,3546,3548],{"class":131,"line":3547},118,[129,3549,3550],{},"        ui.set_visible('my_cool_vec3s', set_viz_a(not viz_a))\n",[129,3552,3554],{"class":131,"line":3553},119,[129,3555,951],{"emptyLinePlaceholder":33},[129,3557,3559],{"class":131,"line":3558},120,[129,3560,3561],{},"    if context.get('name') == 'my_cool_viz_toggle_button2':\n",[129,3563,3565],{"class":131,"line":3564},121,[129,3566,3567],{},"        ui.set_visible('my_cool_string_2', set_viz_b(not viz_b))\n",[129,3569,3571],{"class":131,"line":3570},122,[129,3572,951],{"emptyLinePlaceholder":33},[129,3574,3576],{"class":131,"line":3575},123,[129,3577,951],{"emptyLinePlaceholder":33},[129,3579,3581],{"class":131,"line":3580},124,[129,3582,3583],{},"class CustomComponent(SnoUIComponent):\n",[129,3585,3587],{"class":131,"line":3586},125,[129,3588,3012],{},[129,3590,3592],{"class":131,"line":3591},126,[129,3593,3594],{},"    We can use a custom component for times when we need something more advanced.\n",[129,3596,3598],{"class":131,"line":3597},127,[129,3599,3600],{},"    Just create it wrap it in a hbox or vbox and use it by name in the yaml below\n",[129,3602,3604],{"class":131,"line":3603},128,[129,3605,3012],{},[129,3607,3609],{"class":131,"line":3608},129,[129,3610,951],{"emptyLinePlaceholder":33},[129,3612,3614],{"class":131,"line":3613},130,[129,3615,3616],{},"    # You always need to override build() at least.\n",[129,3618,3620],{"class":131,"line":3619},131,[129,3621,3622],{},"    def build(self) -> SRSX_FMWidgetTransferBox:\n",[129,3624,3626],{"class":131,"line":3625},132,[129,3627,3628],{},"        # Create the faceman component\n",[129,3630,3632],{"class":131,"line":3631},133,[129,3633,3634],{},"        contents = []\n",[129,3636,3638],{"class":131,"line":3637},134,[129,3639,3640],{},"        t = label(self.kwargs.get(\"value\"))\n",[129,3642,3644],{"class":131,"line":3643},135,[129,3645,3646],{},"        contents.append(t)\n",[129,3648,3650],{"class":131,"line":3649},136,[129,3651,3652],{},"        # Add the widget to the widget dict this is used for visibility\n",[129,3654,3656],{"class":131,"line":3655},137,[129,3657,3658],{},"        self.widgets['label'] = t.get_widget()\n",[129,3660,3662],{"class":131,"line":3661},138,[129,3663,951],{"emptyLinePlaceholder":33},[129,3665,3667],{"class":131,"line":3666},139,[129,3668,3669],{},"        # Can also set it as an attribute on the class\n",[129,3671,3673],{"class":131,"line":3672},140,[129,3674,3675],{},"        self.label_widget = self.widgets['label']\n",[129,3677,3679],{"class":131,"line":3678},141,[129,3680,951],{"emptyLinePlaceholder":33},[129,3682,3684],{"class":131,"line":3683},142,[129,3685,3686],{},"        collapsible_t, w = create_collapsible_frame(contents, self.kwargs.get(\"label\"))\n",[129,3688,3690],{"class":131,"line":3689},143,[129,3691,3692],{},"        # return the TransferBox or wrapper transfer box\n",[129,3694,3696],{"class":131,"line":3695},144,[129,3697,3698],{},"        return collapsible_t\n",[129,3700,3702],{"class":131,"line":3701},145,[129,3703,951],{"emptyLinePlaceholder":33},[129,3705,3707],{"class":131,"line":3706},146,[129,3708,3709],{},"    # if we'd like to get and set state we also need to define those with the correct args\n",[129,3711,3713],{"class":131,"line":3712},147,[129,3714,3715],{},"    def get_state(self) -> Optional[bool]:\n",[129,3717,3719],{"class":131,"line":3718},148,[129,3720,3721],{},"        # we can use the attribute here we set\n",[129,3723,3725],{"class":131,"line":3724},149,[129,3726,3727],{},"        return self.label_widget.get_text()\n",[129,3729,3731],{"class":131,"line":3730},150,[129,3732,951],{"emptyLinePlaceholder":33},[129,3734,3736],{"class":131,"line":3735},151,[129,3737,3738],{},"    def set_state(self, state: bool) -> None:\n",[129,3740,3742],{"class":131,"line":3741},152,[129,3743,3744],{},"        # or use the widgets dict\n",[129,3746,3748],{"class":131,"line":3747},153,[129,3749,3750],{},"        self.widgets['label'].set_text(state)\n",[129,3752,3754],{"class":131,"line":3753},154,[129,3755,951],{"emptyLinePlaceholder":33},[129,3757,3759],{"class":131,"line":3758},155,[129,3760,951],{"emptyLinePlaceholder":33},[129,3762,3764],{"class":131,"line":3763},156,[129,3765,3766],{},"def my_example_ui():\n",[129,3768,3770],{"class":131,"line":3769},157,[129,3771,3012],{},[129,3773,3775],{"class":131,"line":3774},158,[129,3776,3777],{},"    This is the main function we run that builds and shows the ui\n",[129,3779,3781],{"class":131,"line":3780},159,[129,3782,3783],{},"    We define how our ui looks in yaml\n",[129,3785,3787],{"class":131,"line":3786},160,[129,3788,3789],{},"    Then connect our tool code functions\n",[129,3791,3793],{"class":131,"line":3792},161,[129,3794,3012],{},[129,3796,3798],{"class":131,"line":3797},162,[129,3799,3800],{},"    # This is the actual UI layout yml code that describes our entire ui\n",[129,3802,3804],{"class":131,"line":3803},163,[129,3805,3806],{},"    #  This could also be a python dict\n",[129,3808,3810],{"class":131,"line":3809},164,[129,3811,3812],{},"    ui_text = \"\"\"\n",[129,3814,3816],{"class":131,"line":3815},165,[129,3817,3818],{},"my_cool_radio:\n",[129,3820,3822],{"class":131,"line":3821},166,[129,3823,3824],{},"    type: radio\n",[129,3826,3828],{"class":131,"line":3827},167,[129,3829,3830],{},"    value: [A, B, See, Dee]\n",[129,3832,3834],{"class":131,"line":3833},168,[129,3835,3836],{},"    label: Radio Group\n",[129,3838,3840],{"class":131,"line":3839},169,[129,3841,3842],{},"my_cool_mat44:\n",[129,3844,3846],{"class":131,"line":3845},170,[129,3847,3848],{},"    type: matrix44\n",[129,3850,3852],{"class":131,"line":3851},171,[129,3853,3854],{},"    label: My Cool Matrix44\n",[129,3856,3858],{"class":131,"line":3857},172,[129,3859,3860],{},"    value: [[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]]\n",[129,3862,3864],{"class":131,"line":3863},173,[129,3865,3866],{},"my_cool_string:\n",[129,3868,3870],{"class":131,"line":3869},174,[129,3871,3872],{},"    type: string\n",[129,3874,3876],{"class":131,"line":3875},175,[129,3877,3878],{},"    label: My Cool String\n",[129,3880,3882],{"class":131,"line":3881},176,[129,3883,3884],{},"    value: Default Value\n",[129,3886,3888],{"class":131,"line":3887},177,[129,3889,3890],{},"my_cool_checkbox:\n",[129,3892,3894],{"class":131,"line":3893},178,[129,3895,3896],{},"    type: checkbox\n",[129,3898,3900],{"class":131,"line":3899},179,[129,3901,3902],{},"    label: My Cool Checkbox\n",[129,3904,3906],{"class":131,"line":3905},180,[129,3907,3908],{},"    value: false\n",[129,3910,3912],{"class":131,"line":3911},181,[129,3913,3914],{},"div2:\n",[129,3916,3918],{"class":131,"line":3917},182,[129,3919,3920],{},"    type: divider\n",[129,3922,3924],{"class":131,"line":3923},183,[129,3925,3926],{},"my_cool_vector3:\n",[129,3928,3930],{"class":131,"line":3929},184,[129,3931,3932],{},"    type: vector3\n",[129,3934,3936],{"class":131,"line":3935},185,[129,3937,3938],{},"    label: My Cool Vec3\n",[129,3940,3942],{"class":131,"line":3941},186,[129,3943,3944],{},"    value: [1.5, 0, 1]\n",[129,3946,3948],{"class":131,"line":3947},187,[129,3949,3950],{},"my_cool_vector4:\n",[129,3952,3954],{"class":131,"line":3953},188,[129,3955,3956],{},"    type: vector4\n",[129,3958,3960],{"class":131,"line":3959},189,[129,3961,3962],{},"    label: My Cool Vec4\n",[129,3964,3966],{"class":131,"line":3965},190,[129,3967,3968],{},"    value: [1.5, 0, 1, 1]\n",[129,3970,3972],{"class":131,"line":3971},191,[129,3973,3974],{},"my_cool_vector2:\n",[129,3976,3978],{"class":131,"line":3977},192,[129,3979,3980],{},"    type: vector2\n",[129,3982,3984],{"class":131,"line":3983},193,[129,3985,3986],{},"    label: My Cool Vec2\n",[129,3988,3990],{"class":131,"line":3989},194,[129,3991,3992],{},"    value: [1.5, 0]\n",[129,3994,3996],{"class":131,"line":3995},195,[129,3997,3998],{},"my_cool_float:\n",[129,4000,4002],{"class":131,"line":4001},196,[129,4003,4004],{},"    type: float\n",[129,4006,4008],{"class":131,"line":4007},197,[129,4009,4010],{},"    label: My Cool Float\n",[129,4012,4014],{"class":131,"line":4013},198,[129,4015,4016],{},"    value: 1.5\n",[129,4018,4020],{"class":131,"line":4019},199,[129,4021,4022],{},"my_cool_label:\n",[129,4024,4026],{"class":131,"line":4025},200,[129,4027,4028],{},"    type: label\n",[129,4030,4032],{"class":131,"line":4031},201,[129,4033,4034],{},"    label: Label For Slider Joined \n",[129,4036,4038],{"class":131,"line":4037},202,[129,4039,4040],{},"    join: True\n",[129,4042,4044],{"class":131,"line":4043},203,[129,4045,4046],{},"my_cool_slider:\n",[129,4048,4050],{"class":131,"line":4049},204,[129,4051,4052],{},"    type: slider\n",[129,4054,4056],{"class":131,"line":4055},205,[129,4057,4058],{},"    min: 0\n",[129,4060,4062],{"class":131,"line":4061},206,[129,4063,4064],{},"    max: 10\n",[129,4066,4068],{"class":131,"line":4067},207,[129,4069,4070],{},"    step: .5\n",[129,4072,4074],{"class":131,"line":4073},208,[129,4075,4076],{},"    value: 5\n",[129,4078,4080],{"class":131,"line":4079},209,[129,4081,4082],{},"div1:\n",[129,4084,4086],{"class":131,"line":4085},210,[129,4087,3920],{},[129,4089,4091],{"class":131,"line":4090},211,[129,4092,4093],{},"label:\n",[129,4095,4097],{"class":131,"line":4096},212,[129,4098,4028],{},[129,4100,4102],{"class":131,"line":4101},213,[129,4103,4104],{},"    label: This is a label\n",[129,4106,4108],{"class":131,"line":4107},214,[129,4109,4110],{},"my_cool_dropdown:\n",[129,4112,4114],{"class":131,"line":4113},215,[129,4115,4116],{},"    type: dropdown\n",[129,4118,4120],{"class":131,"line":4119},216,[129,4121,4122],{},"    label: Dropdown\n",[129,4124,4126],{"class":131,"line":4125},217,[129,4127,4128],{},"    value: ['Item1', 'Item2']\n",[129,4130,4132],{"class":131,"line":4131},218,[129,4133,4134],{},"my_cool_button:\n",[129,4136,4138],{"class":131,"line":4137},219,[129,4139,4140],{},"    type: button\n",[129,4142,4144],{"class":131,"line":4143},220,[129,4145,4146],{},"    label: Joined To Next\n",[129,4148,4150],{"class":131,"line":4149},221,[129,4151,4040],{},[129,4153,4155],{"class":131,"line":4154},222,[129,4156,4157],{},"my_cool_random_button:\n",[129,4159,4161],{"class":131,"line":4160},223,[129,4162,4140],{},[129,4164,4166],{"class":131,"line":4165},224,[129,4167,4168],{},"    label: Random Values\n",[129,4170,4172],{"class":131,"line":4171},225,[129,4173,4040],{},[129,4175,4177],{"class":131,"line":4176},226,[129,4178,4179],{},"my_cool_viz_toggle_button:\n",[129,4181,4183],{"class":131,"line":4182},227,[129,4184,4140],{},[129,4186,4188],{"class":131,"line":4187},228,[129,4189,4190],{},"    label: I Toggle Visibility\n",[129,4192,4194],{"class":131,"line":4193},229,[129,4195,4196],{},"    join: true\n",[129,4198,4200],{"class":131,"line":4199},230,[129,4201,4202],{},"my_cool_viz_toggle_button2:\n",[129,4204,4206],{"class":131,"line":4205},231,[129,4207,4140],{},[129,4209,4211],{"class":131,"line":4210},232,[129,4212,4213],{},"    label: I Also Toggle Visibility\n",[129,4215,4217],{"class":131,"line":4216},233,[129,4218,4219],{},"my_cool_vec3s:\n",[129,4221,4223],{"class":131,"line":4222},234,[129,4224,4225],{},"    type: vector3s\n",[129,4227,4229],{"class":131,"line":4228},235,[129,4230,4231],{},"    label: Vector3 String\n",[129,4233,4235],{"class":131,"line":4234},236,[129,4236,4237],{},"    value: [a, b, c]\n",[129,4239,4241],{"class":131,"line":4240},237,[129,4242,4243],{},"my_cool_string_2:\n",[129,4245,4247],{"class":131,"line":4246},238,[129,4248,3872],{},[129,4250,4252],{"class":131,"line":4251},239,[129,4253,4254],{},"    label: A String That Shows up\n",[129,4256,4258],{"class":131,"line":4257},240,[129,4259,4260],{},"    value: I just showed up!\n",[129,4262,4264],{"class":131,"line":4263},241,[129,4265,4266],{},"    visible: False\n",[129,4268,4270],{"class":131,"line":4269},242,[129,4271,4272],{},"my_progress:\n",[129,4274,4276],{"class":131,"line":4275},243,[129,4277,4278],{},"    type: progress\n",[129,4280,4282],{"class":131,"line":4281},244,[129,4283,4284],{},"my_custom_component:\n",[129,4286,4288],{"class":131,"line":4287},245,[129,4289,4290],{},"    type: my_custom_component\n",[129,4292,4294],{"class":131,"line":4293},246,[129,4295,4296],{},"    label: Custom Component\n",[129,4298,4300],{"class":131,"line":4299},247,[129,4301,4302],{},"    value: [1,2,3]\n",[129,4304,4306],{"class":131,"line":4305},248,[129,4307,4308],{},"my_data_component:\n",[129,4310,4312],{"class":131,"line":4311},249,[129,4313,4314],{},"    type: data\n",[129,4316,4318],{"class":131,"line":4317},250,[129,4319,4320],{},"    value: whatever you want\n",[129,4322,4324],{"class":131,"line":4323},251,[129,4325,4326],{},"my_collapsible:\n",[129,4328,4330],{"class":131,"line":4329},252,[129,4331,4332],{},"    type: collapsible\n",[129,4334,4336],{"class":131,"line":4335},253,[129,4337,4338],{},"    label: Collapsible 1\n",[129,4340,4342],{"class":131,"line":4341},254,[129,4343,4344],{},"    children:\n",[129,4346,4348],{"class":131,"line":4347},255,[129,4349,4350],{},"        my_nested_comp_a:\n",[129,4352,4354],{"class":131,"line":4353},256,[129,4355,4356],{},"            type: string\n",[129,4358,4360],{"class":131,"line":4359},257,[129,4361,4362],{},"            label: String Component A\n",[129,4364,4366],{"class":131,"line":4365},258,[129,4367,4368],{},"            value: A String Component A\n",[129,4370,4372],{"class":131,"line":4371},259,[129,4373,4374],{},"        my_nested_comp_b:\n",[129,4376,4378],{"class":131,"line":4377},260,[129,4379,4380],{},"            type: float\n",[129,4382,4384],{"class":131,"line":4383},261,[129,4385,4386],{},"            label: Float Component B\n",[129,4388,4390],{"class":131,"line":4389},262,[129,4391,4392],{},"            value: 1.0\n",[129,4394,4396],{"class":131,"line":4395},263,[129,4397,4398],{},"my_collapsible2:\n",[129,4400,4402],{"class":131,"line":4401},264,[129,4403,4332],{},[129,4405,4407],{"class":131,"line":4406},265,[129,4408,4409],{},"    label: Collapsible 2\n",[129,4411,4413],{"class":131,"line":4412},266,[129,4414,4344],{},[129,4416,4418],{"class":131,"line":4417},267,[129,4419,4420],{},"        my_nested_comp_c:\n",[129,4422,4424],{"class":131,"line":4423},268,[129,4425,4356],{},[129,4427,4429],{"class":131,"line":4428},269,[129,4430,4431],{},"            label: String Component C\n",[129,4433,4435],{"class":131,"line":4434},270,[129,4436,4437],{},"            value: A String Component C\n",[129,4439,4441],{"class":131,"line":4440},271,[129,4442,4443],{},"        my_nested_comp_d:\n",[129,4445,4447],{"class":131,"line":4446},272,[129,4448,4380],{},[129,4450,4452],{"class":131,"line":4451},273,[129,4453,4454],{},"            label: Float Component D\n",[129,4456,4458],{"class":131,"line":4457},274,[129,4459,4392],{},[129,4461,4463],{"class":131,"line":4462},275,[129,4464,2922],{},[129,4466,4468],{"class":131,"line":4467},276,[129,4469,4470],{},"    # First we instantiate the ui\n",[129,4472,4474],{"class":131,"line":4473},277,[129,4475,4476],{},"    ui = SnoUI(\n",[129,4478,4480],{"class":131,"line":4479},278,[129,4481,4482],{},"        \"My Cool Script Title\",\n",[129,4484,4486],{"class":131,"line":4485},279,[129,4487,4488],{},"        ui_text,\n",[129,4490,4492],{"class":131,"line":4491},280,[129,4493,4494],{},"        log=True,  # adding log=True is usually a good idea\n",[129,4496,4498],{"class":131,"line":4497},281,[129,4499,4500],{},"        # Add in a dict with the type of the component and the Class Reference to add custom components\n",[129,4502,4504],{"class":131,"line":4503},282,[129,4505,4506],{},"        components={'my_custom_component': CustomComponent},\n",[129,4508,4510],{"class":131,"line":4509},283,[129,4511,4512],{},"        # persist_key=\"example-snoui:2.0.4\",  # Optionally enable persistence with a unique persist key\n",[129,4514,4516],{"class":131,"line":4515},284,[129,4517,3214],{},[129,4519,4521],{"class":131,"line":4520},285,[129,4522,951],{"emptyLinePlaceholder":33},[129,4524,4526],{"class":131,"line":4525},286,[129,4527,4528],{},"    # Then each button can be 'connected' like this by name to a function\n",[129,4530,4532],{"class":131,"line":4531},287,[129,4533,4534],{},"    # When the function is connected it will have the ui instance above passed in to it for you\n",[129,4536,4538],{"class":131,"line":4537},288,[129,4539,4540],{},"    ui.connect(\"my_cool_button\", my_cool_tool)\n",[129,4542,4544],{"class":131,"line":4543},289,[129,4545,4546],{},"    ui.connect(\"my_cool_random_button\", my_cool_random_value_tool)\n",[129,4548,4550],{"class":131,"line":4549},290,[129,4551,4552],{},"    ui.connect(\"my_cool_viz_toggle_button\", handle_ui_update)\n",[129,4554,4556],{"class":131,"line":4555},291,[129,4557,4558],{},"    ui.connect(\"my_cool_viz_toggle_button2\", handle_ui_update)\n",[129,4560,4562],{"class":131,"line":4561},292,[129,4563,951],{"emptyLinePlaceholder":33},[129,4565,4567],{"class":131,"line":4566},293,[129,4568,4569],{},"    # Finally show the UI\n",[129,4571,4573],{"class":131,"line":4572},294,[129,4574,4575],{},"    ui.render()  # Comment me out to debug\n",[129,4577,4579],{"class":131,"line":4578},295,[129,4580,951],{"emptyLinePlaceholder":33},[129,4582,4584],{"class":131,"line":4583},296,[129,4585,4586],{},"    # DEBUG HOW TO\n",[129,4588,4590],{"class":131,"line":4589},297,[129,4591,4592],{},"    #  If we are working on a script and we need to run it using the same inputs to test, it is much easier to\n",[129,4594,4596],{"class":131,"line":4595},298,[129,4597,4598],{},"    #  Comment out the ui.render() function above then explicitly set the state manually and then\n",[129,4600,4602],{"class":131,"line":4601},299,[129,4603,4604],{},"    #  call the tool code function needed passing in the SnoUI instance\n",[129,4606,4608],{"class":131,"line":4607},300,[129,4609,951],{"emptyLinePlaceholder":33},[129,4611,4613],{"class":131,"line":4612},301,[129,4614,4615],{},"    # Uncomment below to debug\n",[129,4617,4619],{"class":131,"line":4618},302,[129,4620,4621],{},"    # ui.set_state('my_cool_vector4', [1, 2, 3, 4])\n",[129,4623,4625],{"class":131,"line":4624},303,[129,4626,4627],{},"    # ui.set_state('my_cool_vector2', [1, 2])\n",[129,4629,4631],{"class":131,"line":4630},304,[129,4632,4633],{},"    # my_cool_tool(ui)\n",[129,4635,4637],{"class":131,"line":4636},305,[129,4638,951],{"emptyLinePlaceholder":33},[129,4640,4642],{"class":131,"line":4641},306,[129,4643,951],{"emptyLinePlaceholder":33},[129,4645,4647],{"class":131,"line":4646},307,[129,4648,4649],{},"# To kick the whole thing off we run the function that calls ui.render()\n",[129,4651,4653],{"class":131,"line":4652},308,[129,4654,4655],{},"my_example_ui()\n",[325,4657,4658],{},"html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":20,"searchDepth":21,"depth":21,"links":4660},[],"A Python library for building tool UIs inside Snowdrop without writing verbose PySide boilerplate. Originally developed during The Division: Heartland and used across multiple Snowdrop titles including XDefiant.","\u002Fimages\u002Fwork\u002Fexample-snoui.png",{"date":4664},"01\u002F01\u002F2023",{"title":1988,"description":4661},[1829,347],"work\u002Fsnoui","lil0tB62SS2DZGHiDANLPbgRFKOnpXVvxaE6zH2-eiY",{"id":4670,"title":4671,"body":4672,"description":4676,"excerpt":23,"extension":24,"headerImage":4696,"images":4697,"meta":4701,"navigation":33,"order":1285,"password":23,"path":4702,"seo":4703,"size":21,"software":4704,"stem":4705,"__hash__":4706},"work\u002Fwork\u002Fthor.md","Thor",{"type":7,"value":4673,"toc":4694},[4674,4677,4680,4688,4691],[10,4675,4676],{},"THOR-12 Auto Shotgun",[10,4678,4679],{},"Based on the AA-12",[10,4681,4682,4683,791],{},"Created with modo almost entirely using the ",[52,4684,4687],{"href":4685,"rel":4686},"https:\u002F\u002Fvaughan3d.gumroad.com\u002Fl\u002FWWlrn?layout=profile",[704],"MOP Booleans kit by William Vaughan and Tor Frick",[10,4689,4690],{},"Substance Painter for the textures.",[10,4692,4693],{},"Used in Miscreated with skins",{"title":20,"searchDepth":21,"depth":21,"links":4695},[],"\u002Fimages\u002Fuploads\u002F5d54346e8c3d4thor_12_header_6947324ff4.png",[4698,4699,4700],"\u002Fimages\u002Fuploads\u002F5d4620bf5b2e2_AA_12_Digital_Camo_Urban_2048_dc27e94096.png","\u002Fimages\u002Fuploads\u002F5d4620beba550_AA_12_2048_3384798a85.png","\u002Fimages\u002Fuploads\u002F5d4620bf10d8a_AA_12_Camo_Spray_Woodland_2048_679bbedb40.png",{"date":32},"\u002Fwork\u002Fthor",{"title":4671,"description":4676},[38,586,39],"work\u002Fthor","YxQe6KW0WVzMrEyJ6bqoAL8vdFcGW2iiQ9DLnQ3xCss",{"id":4708,"title":4709,"body":4710,"description":4714,"excerpt":23,"extension":24,"headerImage":4717,"images":23,"meta":4729,"navigation":33,"order":1108,"password":23,"path":4730,"seo":4731,"size":132,"software":4732,"stem":4733,"__hash__":4734},"work\u002Fwork\u002Fvine-rnd.md","Growing Vines R&D",{"type":7,"value":4711,"toc":4727},[4712,4715,4718,4721,4724],[10,4713,4714],{},"This was some R&D I was doing to get real-time vine growth along either a skinned, or static asset.",[359,4716],{"src":4717},"\u002Fimages\u002Fwork\u002Fvine-rnd.gif",[359,4719],{"src":4720},"\u002Fimages\u002Fwork\u002Fvine-rnd-idea.png",[359,4722],{"src":4723},"\u002Fimages\u002Fwork\u002Fvine-rnd-network.png",[359,4725],{"src":4726},"\u002Fimages\u002Fwork\u002Fvine-rnd-houdini.jpg",{"title":20,"searchDepth":21,"depth":21,"links":4728},[],{"date":69},"\u002Fwork\u002Fvine-rnd",{"title":4709,"description":4714},[74],"work\u002Fvine-rnd","VCoSYh6jWvy5XMLmffrevtvFPD9t5P34t3HsXST7epw",{"id":4736,"title":2681,"body":4737,"description":20,"excerpt":23,"extension":24,"headerImage":4770,"images":23,"meta":4771,"navigation":33,"order":21,"password":23,"path":2680,"seo":4772,"size":132,"software":4773,"stem":4774,"__hash__":4775},"work\u002Fwork\u002Fxdefiant.md",{"type":7,"value":4738,"toc":4765},[4739,4743,4748,4752,4758,4762],[363,4740,4742],{"id":4741},"technical-art-leadership","Technical Art Leadership",[10,4744,4745,4746,791],{},"Managed the San Francisco Technical Art department for XDefiant, Ubisoft's free-to-play arena shooter built in Snowdrop Engine. This role built directly on the Snowdrop pipeline experience from ",[52,4747,55],{"href":54},[363,4749,4751],{"id":4750},"character-pipeline","Character Pipeline",[10,4753,4754,4755,4757],{},"Improved the character pipeline to support the pace of a live-service title, ensuring efficient content delivery from concept through final in-engine integration. Leveraged tools like ",[52,4756,1988],{"href":1987}," and internal pipeline tooling originally developed during Heartland to accelerate workflows.",[363,4759,4761],{"id":4760},"mtx-content","MTX Content",[10,4763,4764],{},"Shipped monetization content across multiple seasons, delivering character skins and vanity items on a consistent cadence alongside the live-service release schedule.",{"title":20,"searchDepth":21,"depth":21,"links":4766},[4767,4768,4769],{"id":4741,"depth":21,"text":4742},{"id":4750,"depth":21,"text":4751},{"id":4760,"depth":21,"text":4761},"\u002Fimages\u002Fwork\u002Fxd-logo.png",{"date":1825},{"title":2681,"description":20},[1829,347,75,2008,2009,2010,2011],"work\u002Fxdefiant","GyvJkWlBC5XQc1P_ajkK16oYP_oWY2MUXaJCI3Bh4i0",1776056431090]