{"TotalCount":38,"Files":[{"Ident":"fieldguide.placement","Path":"Code/Attach/AttachedTweakTarget.cs","FileName":"AttachedTweakTarget.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337697,"Code":"using Sandbox;\r\n\r\nnamespace FieldGuide.Placement;\r\n\r\n/// \u003Csummary\u003E\r\n/// A tweak target whose frame label comes from a live \u003Csee cref=\u0022CharacterAttachPoint\u0022/\u003E rather than a\r\n/// string fixed at registration. Use this for anything hanging off a character.\r\n///\r\n/// Why it exists: an attach point resolves LATE (the model has to pose before its attachment objects\r\n/// and bones can be read), so a frame string captured at registration would say \u0022citizen/root\u0022 forever\r\n/// even after the hand attachment came good. Reading it through the mount means the panel sub-line, the\r\n/// export, and the bake comment all name the frame the accessory actually ended up on.\r\n/// \u003C/summary\u003E\r\npublic sealed class AttachedTweakTarget : ITweakTarget\r\n{\r\n\tpublic string DisplayName { get; set; }\r\n\r\n\t/// \u003Csummary\u003EThe accessory whose LOCAL transform the panel edits. Parent it under\r\n\t/// \u003Csee cref=\u0022Mount\u0022/\u003E before registering, or its offsets mean nothing.\u003C/summary\u003E\r\n\tpublic GameObject Target { get; set; }\r\n\r\n\t/// \u003Csummary\u003EThe mount the offset is measured from. Its resolved frame is read live.\u003C/summary\u003E\r\n\tpublic CharacterAttachPoint Mount { get; set; }\r\n\r\n\t/// \u003Csummary\u003EThe id the bake line names. Leave null to derive it from \u003Csee cref=\u0022DisplayName\u0022/\u003E.\u003C/summary\u003E\r\n\tpublic string Symbol { get; set; }\r\n\r\n\t/// \u003Csummary\u003ESlider bounds and steps. Accessory scale by default, which is the point of this type.\u003C/summary\u003E\r\n\tpublic TweakRanges Ranges { get; set; } = TweakRanges.Accessory;\r\n\r\n\tstring ITweakTarget.FrameName =\u003E Mount.IsValid() ? Mount.FrameName : BakeFormat.LocalFrame;\r\n\tstring ITweakTarget.BakeSymbol =\u003E string.IsNullOrEmpty( Symbol ) ? BakeFormat.SymbolFrom( DisplayName ) : Symbol;\r\n\tTweakRanges ITweakTarget.Ranges =\u003E Ranges ?? TweakRanges.Accessory;\r\n\r\n\tpublic AttachedTweakTarget( GameObject accessory, CharacterAttachPoint mount, string name = null )\r\n\t{\r\n\t\tTarget = accessory;\r\n\t\tMount = mount;\r\n\t\tDisplayName = string.IsNullOrEmpty( name ) ? (accessory?.Name ?? \u0022Accessory\u0022) : name;\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.placement","Path":"Code/Demo/DemoBootstrap.cs","FileName":"DemoBootstrap.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337697,"Code":"using Sandbox;\r\nusing System.Collections.Generic;\r\n\r\nnamespace FieldGuide.Placement;\r\n\r\n/// \u003Csummary\u003E\r\n/// Wires the demo scene in code so the whole kit is exercised from one component.\r\n///\r\n/// The scene it builds is the kit\u0027s hero case: a dressed stock citizen standing in the middle, with\r\n/// three accessories parented to mounts on its skeleton (a tool in the right hand, a hat on the head, a\r\n/// pack on the spine). Each is registered with the \u003Csee cref=\u0022TweakSession\u0022/\u003E, so pressing P gives you\r\n/// three tabs of sliders that move the accessory RELATIVE to the bone it hangs from, and Copy hands you\r\n/// the paste-ready offset for your own game code. Everything ships with the engine: the citizen model,\r\n/// its clothing, and the dev primitives standing in for your accessories.\r\n///\r\n/// TWO-LEVEL ACCESSORY SHAPE. Each accessory is a bare root GameObject holding one or more child parts\r\n/// that carry the models. The split is load-bearing: the tweak panel\u0027s Scale row writes a UNIFORM\r\n/// LocalScale on the root, so any non-uniform proportions put there would be flattened the first time\r\n/// someone touched the slider. Proportions live on the children, authored directly in engine units; the\r\n/// root\u0027s scale stays a clean multiplier that reads 1 at the authored size.\r\n///\r\n/// The ghost-placement flow is still here as the second beat: press B and drop boxes on the ground, and\r\n/// they export as world-space placements alongside the accessory offsets.\r\n///\r\n/// Not part of the kit\u0027s runtime surface: delete Code/Demo when you drop the kit into your own project.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Placement Demo Bootstrap\u0022 )]\r\n[Category( \u0022Field Guide \u00B7 Placement\u0022 )]\r\n[Icon( \u0022auto_awesome\u0022 )]\r\npublic sealed class DemoBootstrap : Component\r\n{\r\n\t/// \u003Csummary\u003ERadius (engine units) inside which the demo validity seam reports a spot as valid, to show\r\n\t/// the ghost\u0027s green/red tint switching. Set to 0 or negative to allow placement anywhere.\u003C/summary\u003E\r\n\t[Property] public float DemoValidRadius { get; set; } = 512f;\r\n\r\n\t/// \u003Csummary\u003EYaw the demo citizen faces. 225 puts its front toward the orbit camera\u0027s starting angle.\u003C/summary\u003E\r\n\t[Property] public float CitizenYaw { get; set; } = 225f;\r\n\r\n\tconst string CitizenModel = \u0022models/citizen/citizen.vmdl\u0022;\r\n\tconst string FallbackMaterial = \u0022materials/default.vmat\u0022;\r\n\r\n\t/// \u003Csummary\u003EA plain outfit from the shipped citizen clothing resources. Each item is null-checked, so a\r\n\t/// missing asset degrades to a barer citizen rather than a broken spawn.\u003C/summary\u003E\r\n\tstatic readonly string[] Outfit =\r\n\t{\r\n\t\t\u0022models/citizen_clothes/shirt/Jumpsuit/blue_jumpsuit.clothing\u0022,\r\n\t\t\u0022models/citizen_clothes/shoes/Trainers/trainers.clothing\u0022,\r\n\t};\r\n\r\n\t/// \u003Csummary\u003EAccessories waiting for their mount to resolve before their starting transform is applied.\r\n\t/// See \u003Csee cref=\u0022SeedPending\u0022/\u003E.\u003C/summary\u003E\r\n\treadonly List\u003CPending\u003E _pending = new();\r\n\r\n\t/// \u003Csummary\u003EOne accessory\u0027s starting transform, with two paths. \u003Csee cref=\u0022TunedFrame\u0022/\u003E plus\r\n\t/// \u003Csee cref=\u0022TunedPosition\u0022/\u003E are the values the owner dialled in on the stock citizen and are applied\r\n\t/// verbatim when the mount resolves to exactly that bone. \u003Csee cref=\u0022BodySeed\u0022/\u003E is the generic\r\n\t/// fallback for every other outcome, because bone-local numbers mean nothing on a rig they were not\r\n\t/// measured on.\u003C/summary\u003E\r\n\treadonly record struct Pending(\r\n\t\tGameObject Accessory,\r\n\t\tCharacterAttachPoint Mount,\r\n\t\tstring TunedFrame,\r\n\t\tVector3 TunedPosition,\r\n\t\tAngles TunedAngles,\r\n\t\tfloat TunedScale,\r\n\t\tVector3 BodySeed );\r\n\r\n\t/// \u003Csummary\u003EOne primitive making up an accessory\u0027s silhouette. \u003Csee cref=\u0022SizeUnits\u0022/\u003E is the part\u0027s\r\n\t/// intended size in ENGINE UNITS per axis; the builder divides it by the model\u0027s own bounds to get the\r\n\t/// scale, so the numbers below read as real dimensions rather than model-relative multipliers.\u003C/summary\u003E\r\n\treadonly record struct Part( string ModelPath, Vector3 SizeUnits, Vector3 LocalPosition );\r\n\r\n\tGameObject _citizen;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\tBuildCatalog();\r\n\t\t_citizen = BuildCitizen();\r\n\t\tBuildAccessories( _citizen );\r\n\t\tBuildUi();\r\n\r\n\t\tLog.Info( \u0022[placement] demo ready. The tweak panel is open, P toggles it, right mouse orbits, B enters ghost placement.\u0022 );\r\n\t}\r\n\r\n\tprotected override void OnUpdate() =\u003E SeedPending();\r\n\r\n\t// ---- the ghost-placement catalog (the second beat) ----\r\n\r\n\tvoid BuildCatalog()\r\n\t{\r\n\t\tvar cat = PlacementCatalog.Instance ?? Components.GetOrCreate\u003CPlacementCatalog\u003E();\r\n\t\tcat.Entries.Clear();\r\n\t\tcat.Entries.Add( new PlaceableEntry( \u0022box\u0022, \u0022Box\u0022, \u0022models/dev/box.vmdl\u0022 ) );\r\n\t\tcat.Entries.Add( new PlaceableEntry( \u0022sphere\u0022, \u0022Sphere\u0022, \u0022models/dev/sphere.vmdl\u0022 ) );\r\n\t\tcat.Entries.Add( new PlaceableEntry( \u0022plane\u0022, \u0022Plane\u0022, \u0022models/dev/plane.vmdl\u0022 ) );\r\n\r\n\t\t// Demo validity seam: only allow placement within DemoValidRadius of the origin (illustrates the\r\n\t\t// green/red ghost tint). Replace or clear this in your own project.\r\n\t\tif ( DemoValidRadius \u003E 0f )\r\n\t\t\tcat.ValidityCheck = ( pos, rot ) =\u003E pos.WithZ( 0f ).Length \u003C= DemoValidRadius;\r\n\t}\r\n\r\n\t// ---- the character ----\r\n\r\n\tGameObject BuildCitizen()\r\n\t{\r\n\t\tvar go = Scene.CreateObject();\r\n\t\tgo.Name = \u0022Citizen\u0022;\r\n\t\tgo.WorldPosition = Vector3.Zero;\r\n\t\tgo.WorldRotation = Rotation.FromYaw( CitizenYaw );\r\n\r\n\t\tvar renderer = go.Components.Create\u003CSkinnedModelRenderer\u003E();\r\n\t\tvar model = Model.Load( CitizenModel );\r\n\t\tif ( model is null || model.IsError )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022[placement] demo citizen model \u0027{CitizenModel}\u0027 did not load; the mounts will fall back to the root.\u0022 );\r\n\t\t\treturn go;\r\n\t\t}\r\n\t\trenderer.Model = model;\r\n\t\tDress( renderer );\r\n\r\n\t\t// Standing idle straight off the citizen animgraph: grounded with no move input is its rest state,\r\n\t\t// which breathes and shifts weight a little. That subtle motion is the point here, because an\r\n\t\t// accessory that only looks right on a frozen T-pose is not actually fitted.\r\n\t\trenderer.Set( \u0022b_grounded\u0022, true );\r\n\r\n\t\t// holdtype 6 is Swing on the citizen animgraph: a one-handed closed fist, so the hand actually grips\r\n\t\t// the tool instead of leaving a flat open palm under it. holdtype_handedness 1 is the right hand.\r\n\t\trenderer.Set( \u0022holdtype\u0022, 6 );\r\n\t\trenderer.Set( \u0022holdtype_handedness\u0022, 1 );\r\n\r\n\t\treturn go;\r\n\t}\r\n\r\n\tstatic void Dress( SkinnedModelRenderer renderer )\r\n\t{\r\n\t\tvar outfit = new ClothingContainer();\r\n\t\tbool any = false;\r\n\t\tforeach ( var path in Outfit )\r\n\t\t{\r\n\t\t\tvar item = ResourceLibrary.Get\u003CClothing\u003E( path );\r\n\t\t\tif ( item is null )\r\n\t\t\t{\r\n\t\t\t\tLog.Warning( $\u0022[placement] demo clothing \u0027{path}\u0027 did not resolve, skipping that slot.\u0022 );\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\t\t\toutfit.Add( item );\r\n\t\t\tany = true;\r\n\t\t}\r\n\t\tif ( any ) outfit.Apply( renderer );\r\n\t}\r\n\r\n\t// ---- the three accessories ----\r\n\r\n\tvoid BuildAccessories( GameObject citizen )\r\n\t{\r\n\t\tvar session = TweakSession.Instance ?? Components.GetOrCreate\u003CTweakSession\u003E();\r\n\r\n\t\t// OBSERVED ON THE SHIPPED CITIZEN (2026-07-30): none of the attachment names below resolved, and all\r\n\t\t// three mounts landed on their bone backstop (hand_R, head, spine_2). The attachment candidates stay\r\n\t\t// in the list because they are correct on rigs that do expose them and they exercise the\r\n\t\t// attachment-first path, but the bones are what actually carries this demo.\r\n\r\n\t\t// Right hand. hold_R is the usual weapon-hold attachment name; the casing varies between models.\r\n\t\tvar hand = BuildMount( citizen, \u0022Hand Mount\u0022,\r\n\t\t\tattachments: new List\u003Cstring\u003E { \u0022hold_R\u0022, \u0022hold_r\u0022 },\r\n\t\t\tbones: new List\u003Cstring\u003E { \u0022hand_R\u0022, \u0022hand_r\u0022 } );\r\n\r\n\t\t// Head. \u0022hat\u0022 is the usual head-top attachment point; the head bone is the backstop that resolves here.\r\n\t\tvar head = BuildMount( citizen, \u0022Head Mount\u0022,\r\n\t\t\tattachments: new List\u003Cstring\u003E { \u0022hat\u0022, \u0022head\u0022 },\r\n\t\t\tbones: new List\u003Cstring\u003E { \u0022head\u0022 } );\r\n\r\n\t\t// Upper spine. No attachment here, so this one exercises the bone path: the mount is re-pinned to\r\n\t\t// spine_2 every frame, which is what makes a back-worn item bob with the animation instead of\r\n\t\t// sliding along the root.\r\n\t\tvar back = BuildMount( citizen, \u0022Back Mount\u0022,\r\n\t\t\tattachments: new List\u003Cstring\u003E(),\r\n\t\t\tbones: new List\u003Cstring\u003E { \u0022spine_2\u0022, \u0022spine_1\u0022, \u0022spine_0\u0022, \u0022spine\u0022 } );\r\n\r\n\t\t// SILHOUETTES, and the axis they are built along. All three tuned offsets came back with their large\r\n\t\t// component on local X (hand \u002B5.5, head \u002B15, spine \u002B1.6 with the big move on Y), which is the bone\r\n\t\t// chain running \u002BX down its length: out past the fingers, up out of the skull, up the spine. So each\r\n\t\t// accessory is authored with its long axis on X. If a rig ever disagrees, the panel\u0027s rotation rows\r\n\t\t// fix it in one drag; nothing here depends on the guess being right.\r\n\t\t//\r\n\t\t// SIZES are the ones the owner settled on, in engine units on the longest axis: tool 5, hat 7,\r\n\t\t// pack 14. See BuildPart for why they are written as dimensions rather than scale factors.\r\n\r\n\t\t// Tool: a handle with a heavier head on the end, reading along \u002BX (out past the fingers).\r\n\t\tRegister( session, hand, \u0022Hand Tool\u0022,\r\n\t\t\ttunedFrame: \u0022citizen/hand_R\u0022,\r\n\t\t\ttunedPosition: new Vector3( 4.25f, 0.5f, -2.5f ),\r\n\t\t\ttunedAngles: new Angles( 5f, 91f, 5f ),\r\n\t\t\ttunedScale: 1.9f,\r\n\t\t\tbodySeed: new Vector3( 5f, 0f, -3f ),\r\n\t\t\ttint: new Color( 1f, 0.62f, 0.25f ),\r\n\t\t\tparts: new[]\r\n\t\t\t{\r\n\t\t\t\tnew Part( \u0022models/dev/box.vmdl\u0022, new Vector3( 3.4f, 1f, 1f ), new Vector3( -0.8f, 0f, 0f ) ),\r\n\t\t\t\tnew Part( \u0022models/dev/box.vmdl\u0022, new Vector3( 1.6f, 1.7f, 1.7f ), new Vector3( 1.7f, 0f, 0f ) ),\r\n\t\t\t} );\r\n\r\n\t\t// Hat: a flat brim disk with a dome crown above it. No cylinder ships in models/dev, so both are\r\n\t\t// squashed spheres; the brim is thin on X (the up-out-of-the-skull axis) and round across Y and Z.\r\n\t\tRegister( session, head, \u0022Hat\u0022,\r\n\t\t\ttunedFrame: \u0022citizen/head\u0022,\r\n\t\t\ttunedPosition: new Vector3( 15f, 1.901744f, 0.000369f ),\r\n\t\t\ttunedAngles: new Angles( 0f, 0f, 0f ),\r\n\t\t\ttunedScale: 1f,\r\n\t\t\tbodySeed: new Vector3( 2f, 0f, 6f ),\r\n\t\t\ttint: new Color( 0.42f, 0.86f, 1f ),\r\n\t\t\tparts: new[]\r\n\t\t\t{\r\n\t\t\t\tnew Part( \u0022models/dev/sphere.vmdl\u0022, new Vector3( 1.6f, 7f, 7f ), new Vector3( 0f, 0f, 0f ) ),\r\n\t\t\t\tnew Part( \u0022models/dev/sphere.vmdl\u0022, new Vector3( 2.6f, 4.2f, 4.2f ), new Vector3( 2f, 0f, 0f ) ),\r\n\t\t\t} );\r\n\r\n\t\t// Pack: the box was close, so this is only a proportion change, taller up the spine than it is deep\r\n\t\t// off the back. Kept chunky rather than slab-thin so it still reads as a pack under any axis order.\r\n\t\tRegister( session, back, \u0022Back Pack\u0022,\r\n\t\t\ttunedFrame: \u0022citizen/spine_2\u0022,\r\n\t\t\ttunedPosition: new Vector3( 1.625522f, -9.075111f, -0.00035f ),\r\n\t\t\ttunedAngles: new Angles( 0f, 0f, 0f ),\r\n\t\t\ttunedScale: 1f,\r\n\t\t\tbodySeed: new Vector3( -9f, 0f, 2f ),\r\n\t\t\ttint: new Color( 0.85f, 0.45f, 0.95f ),\r\n\t\t\tparts: new[]\r\n\t\t\t{\r\n\t\t\t\tnew Part( \u0022models/dev/box.vmdl\u0022, new Vector3( 14f, 7f, 11f ), new Vector3( 0f, 0f, 0f ) ),\r\n\t\t\t} );\r\n\t}\r\n\r\n\tCharacterAttachPoint BuildMount( GameObject citizen, string name, List\u003Cstring\u003E attachments, List\u003Cstring\u003E bones )\r\n\t{\r\n\t\tvar go = Scene.CreateObject();\r\n\t\tgo.Name = name;\r\n\t\tgo.SetParent( citizen, false );\r\n\t\tgo.LocalPosition = Vector3.Zero;\r\n\t\tgo.LocalRotation = Rotation.Identity;\r\n\r\n\t\tvar mount = go.Components.Create\u003CCharacterAttachPoint\u003E();\r\n\t\tmount.AttachmentNames = attachments;\r\n\t\tmount.BoneNames = bones;\r\n\t\tmount.CharacterName = \u0022citizen\u0022;\r\n\t\treturn mount;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Build one accessory under a mount and register it for tweaking. The root is a bare GameObject: it\r\n\t/// owns the offset the panel edits and a uniform scale that starts at 1, and nothing else. The parts\r\n\t/// hang under it carrying the models and the proportions.\r\n\t///\r\n\t/// The starting transform is applied later, once the mount resolves. See \u003Csee cref=\u0022SeedPending\u0022/\u003E.\r\n\t/// \u003C/summary\u003E\r\n\tvoid Register( TweakSession session, CharacterAttachPoint mount, string label,\r\n\t\tstring tunedFrame, Vector3 tunedPosition, Angles tunedAngles, float tunedScale, Vector3 bodySeed,\r\n\t\tColor tint, Part[] parts )\r\n\t{\r\n\t\tvar go = Scene.CreateObject();\r\n\t\tgo.Name = label;\r\n\t\tgo.SetParent( mount.GameObject, false );\r\n\t\tgo.LocalPosition = Vector3.Zero;\r\n\t\tgo.LocalRotation = Rotation.Identity;\r\n\t\tgo.LocalScale = Vector3.One;   // the panel\u0027s Scale row is a multiplier on the authored size, so 1 is \u0022as built\u0022\r\n\r\n\t\tfor ( int i = 0; i \u003C parts.Length; i\u002B\u002B )\r\n\t\t\tBuildPart( go, $\u0022{label} part {i \u002B 1}\u0022, parts[i], tint );\r\n\r\n\t\tsession.Add( new AttachedTweakTarget( go, mount, label ) );\r\n\t\t_pending.Add( new Pending( go, mount, tunedFrame, tunedPosition, tunedAngles, tunedScale, bodySeed ) );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// One primitive under an accessory root, sized in engine units.\r\n\t///\r\n\t/// The scale is the requested size divided by the model\u0027s OWN bounds, per axis, rather than a\r\n\t/// hand-tuned multiplier. That keeps the part table readable as real dimensions and survives the engine\r\n\t/// changing what a dev primitive measures: the shipped box is 50 units and the sphere is 64, and\r\n\t/// nothing here has to know that.\r\n\t/// \u003C/summary\u003E\r\n\tvoid BuildPart( GameObject parent, string name, Part part, Color tint )\r\n\t{\r\n\t\tvar go = Scene.CreateObject();\r\n\t\tgo.Name = name;\r\n\t\tgo.SetParent( parent, false );\r\n\t\tgo.LocalPosition = part.LocalPosition;\r\n\t\tgo.LocalRotation = Rotation.Identity;\r\n\r\n\t\tvar renderer = go.Components.Create\u003CModelRenderer\u003E();\r\n\t\tvar model = Model.Load( part.ModelPath );\r\n\t\tif ( model is null || model.IsError )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022[placement] demo part model \u0027{part.ModelPath}\u0027 did not load; \u0027{name}\u0027 will be invisible.\u0022 );\r\n\t\t\treturn;\r\n\t\t}\r\n\t\trenderer.Model = model;\r\n\r\n\t\tvar bounds = model.Bounds.Size;\r\n\t\tgo.LocalScale = new Vector3(\r\n\t\t\tbounds.x \u003E 0.001f ? part.SizeUnits.x / bounds.x : 1f,\r\n\t\t\tbounds.y \u003E 0.001f ? part.SizeUnits.y / bounds.y : 1f,\r\n\t\t\tbounds.z \u003E 0.001f ? part.SizeUnits.z / bounds.z : 1f );\r\n\r\n\t\t// The engine\u0027s models/dev primitives render as missing-material magenta unless a real material is\r\n\t\t// forced on, which would swallow the tint that tells the three accessories apart.\r\n\t\tvar mat = Material.Load( FallbackMaterial );\r\n\t\tif ( mat is not null ) renderer.MaterialOverride = mat;\r\n\t\trenderer.Tint = tint;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Apply each accessory\u0027s starting transform once its mount has resolved, then re-baseline it as the\r\n\t/// authored default so the panel\u0027s Reset comes back here.\r\n\t///\r\n\t/// Two paths, and which one runs depends on what the rig gave us:\r\n\t///\r\n\t///  - The mount landed on exactly the bone the demo values were measured against: apply them verbatim.\r\n\t///    These are real numbers, dialled in on the stock citizen, so the demo opens already fitted and the\r\n\t///    first thing you see is the finished result rather than three primitives in a heap.\r\n\t///  - Anything else, including the character-root fallback and any renamed or substituted bone: fall\r\n\t///    back to a seed expressed in the CHARACTER\u0027s frame (x forward, y left, z up) and let the engine\r\n\t///    convert it to local. Bone-local numbers are only valid on the rig they were measured on; reusing\r\n\t///    them elsewhere buries an accessory in the chest on one rig and flings it into orbit on the next.\r\n\t///    The character frame is not accurate, but it is always visible, which is all a fallback owes you.\r\n\t///\r\n\t/// The frame test requires a BONE mount, not just a matching name. An attachment that happens to be\r\n\t/// called \u0022head\u0022 is a different transform from the head bone, and the tuned numbers would be wrong on it.\r\n\t/// \u003C/summary\u003E\r\n\tvoid SeedPending()\r\n\t{\r\n\t\tif ( _pending.Count == 0 ) return;\r\n\r\n\t\tvar session = TweakSession.Instance;\r\n\t\tvar bodyRot = _citizen.IsValid() ? _citizen.WorldRotation : Rotation.Identity;\r\n\r\n\t\tfor ( int i = _pending.Count - 1; i \u003E= 0; i-- )\r\n\t\t{\r\n\t\t\tvar p = _pending[i];\r\n\t\t\tif ( !p.Accessory.IsValid() || !p.Mount.IsValid() ) { _pending.RemoveAt( i ); continue; }\r\n\t\t\tif ( p.Mount.Kind == CharacterAttachPoint.MountKind.Unresolved ) continue;\r\n\r\n\t\t\tbool tuned = p.Mount.Kind == CharacterAttachPoint.MountKind.Bone\r\n\t\t\t\t\u0026\u0026 p.Mount.FrameName == p.TunedFrame;\r\n\r\n\t\t\tif ( tuned )\r\n\t\t\t{\r\n\t\t\t\tp.Accessory.LocalPosition = p.TunedPosition;\r\n\t\t\t\tp.Accessory.LocalRotation = p.TunedAngles.ToRotation();\r\n\t\t\t\tp.Accessory.LocalScale = new Vector3( p.TunedScale, p.TunedScale, p.TunedScale );\r\n\t\t\t\tLog.Info( $\u0022[placement] \u0027{p.Accessory.Name}\u0027 seated at the tuned offset for {p.TunedFrame}\u0022 );\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tp.Accessory.WorldPosition = p.Mount.WorldPosition \u002B bodyRot * p.BodySeed;\r\n\t\t\t\tLog.Info( $\u0022[placement] \u0027{p.Accessory.Name}\u0027 mounted on {p.Mount.FrameName}, not the tuned \u0022\r\n\t\t\t\t\t\u002B $\u0022{p.TunedFrame}; using the generic character-frame seed instead. Fit it with P.\u0022 );\r\n\t\t\t}\r\n\r\n\t\t\tsession?.CaptureSeed( p.Accessory );\r\n\t\t\t_pending.RemoveAt( i );\r\n\t\t}\r\n\t}\r\n\r\n\t// ---- screen UI ----\r\n\r\n\tvoid BuildUi()\r\n\t{\r\n\t\t// One ScreenPanel per PanelComponent (the World Builder UI idiom). Built in code so the demo scene\r\n\t\t// needs no razor wiring.\r\n\t\tvar panelHost = Scene.CreateObject();\r\n\t\tpanelHost.Name = \u0022Placement UI\u0022;\r\n\t\tpanelHost.Components.Create\u003CScreenPanel\u003E();\r\n\r\n\t\t// Open on arrival. Fitting the accessories is what this scene is FOR, so making the visitor find the\r\n\t\t// key first is a toll booth on the way to the point. P and the header x still close it. The panel\r\n\t\t// reads this on its first update, after every OnStart in the frame, so setting it here always lands.\r\n\t\tvar panel = panelHost.Components.Create\u003CTweakPanel\u003E();\r\n\t\tpanel.OpenOnStart = true;\r\n\r\n\t\tvar hintHost = Scene.CreateObject();\r\n\t\thintHost.Name = \u0022Placement Hint\u0022;\r\n\t\thintHost.Components.Create\u003CScreenPanel\u003E();\r\n\t\thintHost.Components.Create\u003CDemoHintCard\u003E();\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.placement","Path":"Code/Placement/PlacedInstance.cs","FileName":"PlacedInstance.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337697,"Code":"using Sandbox;\r\n\r\nnamespace FieldGuide.Placement;\r\n\r\n/// \u003Csummary\u003E\r\n/// Tags a placed object with the catalog id it came from, so \u003Csee cref=\u0022PlacementExport\u0022/\u003E can collect\r\n/// every placed object in the scene and record which entry produced it. \u003Csee cref=\u0022GhostPlacer\u0022/\u003E adds\r\n/// this on place; you can also add it by hand (or in a prefab) to make an object show up in exports.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Placed Instance\u0022 )]\r\n[Category( \u0022Field Guide \u00B7 Placement\u0022 )]\r\n[Icon( \u0022place\u0022 )]\r\npublic sealed class PlacedInstance : Component\r\n{\r\n\t/// \u003Csummary\u003EThe \u003Csee cref=\u0022PlaceableEntry.Id\u0022/\u003E this object was placed from.\u003C/summary\u003E\r\n\t[Property] public string CatalogId { get; set; }\r\n}\r\n"},{"Ident":"fieldguide.placement","Path":"Demo/DemoBootstrap.cs","FileName":"DemoBootstrap.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337697,"Code":"using Sandbox;\r\nusing System.Collections.Generic;\r\n\r\nnamespace FieldGuide.Placement;\r\n\r\n/// \u003Csummary\u003E\r\n/// Wires the demo scene in code so the whole kit is exercised from one component.\r\n///\r\n/// The scene it builds is the kit\u0027s hero case: a dressed stock citizen standing in the middle, with\r\n/// three accessories parented to mounts on its skeleton (a tool in the right hand, a hat on the head, a\r\n/// pack on the spine). Each is registered with the \u003Csee cref=\u0022TweakSession\u0022/\u003E, so pressing P gives you\r\n/// three tabs of sliders that move the accessory RELATIVE to the bone it hangs from, and Copy hands you\r\n/// the paste-ready offset for your own game code. Everything ships with the engine: the citizen model,\r\n/// its clothing, and the dev primitives standing in for your accessories.\r\n///\r\n/// TWO-LEVEL ACCESSORY SHAPE. Each accessory is a bare root GameObject holding one or more child parts\r\n/// that carry the models. The split is load-bearing: the tweak panel\u0027s Scale row writes a UNIFORM\r\n/// LocalScale on the root, so any non-uniform proportions put there would be flattened the first time\r\n/// someone touched the slider. Proportions live on the children, authored directly in engine units; the\r\n/// root\u0027s scale stays a clean multiplier that reads 1 at the authored size.\r\n///\r\n/// The ghost-placement flow is still here as the second beat: press B and drop boxes on the ground, and\r\n/// they export as world-space placements alongside the accessory offsets.\r\n///\r\n/// Not part of the kit\u0027s runtime surface: delete Code/Demo when you drop the kit into your own project.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Placement Demo Bootstrap\u0022 )]\r\n[Category( \u0022Field Guide \u00B7 Placement\u0022 )]\r\n[Icon( \u0022auto_awesome\u0022 )]\r\npublic sealed class DemoBootstrap : Component\r\n{\r\n\t/// \u003Csummary\u003ERadius (engine units) inside which the demo validity seam reports a spot as valid, to show\r\n\t/// the ghost\u0027s green/red tint switching. Set to 0 or negative to allow placement anywhere.\u003C/summary\u003E\r\n\t[Property] public float DemoValidRadius { get; set; } = 512f;\r\n\r\n\t/// \u003Csummary\u003EYaw the demo citizen faces. 225 puts its front toward the orbit camera\u0027s starting angle.\u003C/summary\u003E\r\n\t[Property] public float CitizenYaw { get; set; } = 225f;\r\n\r\n\tconst string CitizenModel = \u0022models/citizen/citizen.vmdl\u0022;\r\n\tconst string FallbackMaterial = \u0022materials/default.vmat\u0022;\r\n\r\n\t/// \u003Csummary\u003EA plain outfit from the shipped citizen clothing resources. Each item is null-checked, so a\r\n\t/// missing asset degrades to a barer citizen rather than a broken spawn.\u003C/summary\u003E\r\n\tstatic readonly string[] Outfit =\r\n\t{\r\n\t\t\u0022models/citizen_clothes/shirt/Jumpsuit/blue_jumpsuit.clothing\u0022,\r\n\t\t\u0022models/citizen_clothes/shoes/Trainers/trainers.clothing\u0022,\r\n\t};\r\n\r\n\t/// \u003Csummary\u003EAccessories waiting for their mount to resolve before their starting transform is applied.\r\n\t/// See \u003Csee cref=\u0022SeedPending\u0022/\u003E.\u003C/summary\u003E\r\n\treadonly List\u003CPending\u003E _pending = new();\r\n\r\n\t/// \u003Csummary\u003EOne accessory\u0027s starting transform, with two paths. \u003Csee cref=\u0022TunedFrame\u0022/\u003E plus\r\n\t/// \u003Csee cref=\u0022TunedPosition\u0022/\u003E are the values the owner dialled in on the stock citizen and are applied\r\n\t/// verbatim when the mount resolves to exactly that bone. \u003Csee cref=\u0022BodySeed\u0022/\u003E is the generic\r\n\t/// fallback for every other outcome, because bone-local numbers mean nothing on a rig they were not\r\n\t/// measured on.\u003C/summary\u003E\r\n\treadonly record struct Pending(\r\n\t\tGameObject Accessory,\r\n\t\tCharacterAttachPoint Mount,\r\n\t\tstring TunedFrame,\r\n\t\tVector3 TunedPosition,\r\n\t\tAngles TunedAngles,\r\n\t\tfloat TunedScale,\r\n\t\tVector3 BodySeed );\r\n\r\n\t/// \u003Csummary\u003EOne primitive making up an accessory\u0027s silhouette. \u003Csee cref=\u0022SizeUnits\u0022/\u003E is the part\u0027s\r\n\t/// intended size in ENGINE UNITS per axis; the builder divides it by the model\u0027s own bounds to get the\r\n\t/// scale, so the numbers below read as real dimensions rather than model-relative multipliers.\u003C/summary\u003E\r\n\treadonly record struct Part( string ModelPath, Vector3 SizeUnits, Vector3 LocalPosition );\r\n\r\n\tGameObject _citizen;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\tBuildCatalog();\r\n\t\t_citizen = BuildCitizen();\r\n\t\tBuildAccessories( _citizen );\r\n\t\tBuildUi();\r\n\r\n\t\tLog.Info( \u0022[placement] demo ready. The tweak panel is open, P toggles it, right mouse orbits, B enters ghost placement.\u0022 );\r\n\t}\r\n\r\n\tprotected override void OnUpdate() =\u003E SeedPending();\r\n\r\n\t// ---- the ghost-placement catalog (the second beat) ----\r\n\r\n\tvoid BuildCatalog()\r\n\t{\r\n\t\tvar cat = PlacementCatalog.Instance ?? Components.GetOrCreate\u003CPlacementCatalog\u003E();\r\n\t\tcat.Entries.Clear();\r\n\t\tcat.Entries.Add( new PlaceableEntry( \u0022box\u0022, \u0022Box\u0022, \u0022models/dev/box.vmdl\u0022 ) );\r\n\t\tcat.Entries.Add( new PlaceableEntry( \u0022sphere\u0022, \u0022Sphere\u0022, \u0022models/dev/sphere.vmdl\u0022 ) );\r\n\t\tcat.Entries.Add( new PlaceableEntry( \u0022plane\u0022, \u0022Plane\u0022, \u0022models/dev/plane.vmdl\u0022 ) );\r\n\r\n\t\t// Demo validity seam: only allow placement within DemoValidRadius of the origin (illustrates the\r\n\t\t// green/red ghost tint). Replace or clear this in your own project.\r\n\t\tif ( DemoValidRadius \u003E 0f )\r\n\t\t\tcat.ValidityCheck = ( pos, rot ) =\u003E pos.WithZ( 0f ).Length \u003C= DemoValidRadius;\r\n\t}\r\n\r\n\t// ---- the character ----\r\n\r\n\tGameObject BuildCitizen()\r\n\t{\r\n\t\tvar go = Scene.CreateObject();\r\n\t\tgo.Name = \u0022Citizen\u0022;\r\n\t\tgo.WorldPosition = Vector3.Zero;\r\n\t\tgo.WorldRotation = Rotation.FromYaw( CitizenYaw );\r\n\r\n\t\tvar renderer = go.Components.Create\u003CSkinnedModelRenderer\u003E();\r\n\t\tvar model = Model.Load( CitizenModel );\r\n\t\tif ( model is null || model.IsError )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022[placement] demo citizen model \u0027{CitizenModel}\u0027 did not load; the mounts will fall back to the root.\u0022 );\r\n\t\t\treturn go;\r\n\t\t}\r\n\t\trenderer.Model = model;\r\n\t\tDress( renderer );\r\n\r\n\t\t// Standing idle straight off the citizen animgraph: grounded with no move input is its rest state,\r\n\t\t// which breathes and shifts weight a little. That subtle motion is the point here, because an\r\n\t\t// accessory that only looks right on a frozen T-pose is not actually fitted.\r\n\t\trenderer.Set( \u0022b_grounded\u0022, true );\r\n\r\n\t\t// holdtype 6 is Swing on the citizen animgraph: a one-handed closed fist, so the hand actually grips\r\n\t\t// the tool instead of leaving a flat open palm under it. holdtype_handedness 1 is the right hand.\r\n\t\trenderer.Set( \u0022holdtype\u0022, 6 );\r\n\t\trenderer.Set( \u0022holdtype_handedness\u0022, 1 );\r\n\r\n\t\treturn go;\r\n\t}\r\n\r\n\tstatic void Dress( SkinnedModelRenderer renderer )\r\n\t{\r\n\t\tvar outfit = new ClothingContainer();\r\n\t\tbool any = false;\r\n\t\tforeach ( var path in Outfit )\r\n\t\t{\r\n\t\t\tvar item = ResourceLibrary.Get\u003CClothing\u003E( path );\r\n\t\t\tif ( item is null )\r\n\t\t\t{\r\n\t\t\t\tLog.Warning( $\u0022[placement] demo clothing \u0027{path}\u0027 did not resolve, skipping that slot.\u0022 );\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\t\t\toutfit.Add( item );\r\n\t\t\tany = true;\r\n\t\t}\r\n\t\tif ( any ) outfit.Apply( renderer );\r\n\t}\r\n\r\n\t// ---- the three accessories ----\r\n\r\n\tvoid BuildAccessories( GameObject citizen )\r\n\t{\r\n\t\tvar session = TweakSession.Instance ?? Components.GetOrCreate\u003CTweakSession\u003E();\r\n\r\n\t\t// OBSERVED ON THE SHIPPED CITIZEN (2026-07-30): none of the attachment names below resolved, and all\r\n\t\t// three mounts landed on their bone backstop (hand_R, head, spine_2). The attachment candidates stay\r\n\t\t// in the list because they are correct on rigs that do expose them and they exercise the\r\n\t\t// attachment-first path, but the bones are what actually carries this demo.\r\n\r\n\t\t// Right hand. hold_R is the usual weapon-hold attachment name; the casing varies between models.\r\n\t\tvar hand = BuildMount( citizen, \u0022Hand Mount\u0022,\r\n\t\t\tattachments: new List\u003Cstring\u003E { \u0022hold_R\u0022, \u0022hold_r\u0022 },\r\n\t\t\tbones: new List\u003Cstring\u003E { \u0022hand_R\u0022, \u0022hand_r\u0022 } );\r\n\r\n\t\t// Head. \u0022hat\u0022 is the usual head-top attachment point; the head bone is the backstop that resolves here.\r\n\t\tvar head = BuildMount( citizen, \u0022Head Mount\u0022,\r\n\t\t\tattachments: new List\u003Cstring\u003E { \u0022hat\u0022, \u0022head\u0022 },\r\n\t\t\tbones: new List\u003Cstring\u003E { \u0022head\u0022 } );\r\n\r\n\t\t// Upper spine. No attachment here, so this one exercises the bone path: the mount is re-pinned to\r\n\t\t// spine_2 every frame, which is what makes a back-worn item bob with the animation instead of\r\n\t\t// sliding along the root.\r\n\t\tvar back = BuildMount( citizen, \u0022Back Mount\u0022,\r\n\t\t\tattachments: new List\u003Cstring\u003E(),\r\n\t\t\tbones: new List\u003Cstring\u003E { \u0022spine_2\u0022, \u0022spine_1\u0022, \u0022spine_0\u0022, \u0022spine\u0022 } );\r\n\r\n\t\t// SILHOUETTES, and the axis they are built along. All three tuned offsets came back with their large\r\n\t\t// component on local X (hand \u002B5.5, head \u002B15, spine \u002B1.6 with the big move on Y), which is the bone\r\n\t\t// chain running \u002BX down its length: out past the fingers, up out of the skull, up the spine. So each\r\n\t\t// accessory is authored with its long axis on X. If a rig ever disagrees, the panel\u0027s rotation rows\r\n\t\t// fix it in one drag; nothing here depends on the guess being right.\r\n\t\t//\r\n\t\t// SIZES are the ones the owner settled on, in engine units on the longest axis: tool 5, hat 7,\r\n\t\t// pack 14. See BuildPart for why they are written as dimensions rather than scale factors.\r\n\r\n\t\t// Tool: a handle with a heavier head on the end, reading along \u002BX (out past the fingers).\r\n\t\tRegister( session, hand, \u0022Hand Tool\u0022,\r\n\t\t\ttunedFrame: \u0022citizen/hand_R\u0022,\r\n\t\t\ttunedPosition: new Vector3( 4.25f, 0.5f, -2.5f ),\r\n\t\t\ttunedAngles: new Angles( 5f, 91f, 5f ),\r\n\t\t\ttunedScale: 1.9f,\r\n\t\t\tbodySeed: new Vector3( 5f, 0f, -3f ),\r\n\t\t\ttint: new Color( 1f, 0.62f, 0.25f ),\r\n\t\t\tparts: new[]\r\n\t\t\t{\r\n\t\t\t\tnew Part( \u0022models/dev/box.vmdl\u0022, new Vector3( 3.4f, 1f, 1f ), new Vector3( -0.8f, 0f, 0f ) ),\r\n\t\t\t\tnew Part( \u0022models/dev/box.vmdl\u0022, new Vector3( 1.6f, 1.7f, 1.7f ), new Vector3( 1.7f, 0f, 0f ) ),\r\n\t\t\t} );\r\n\r\n\t\t// Hat: a flat brim disk with a dome crown above it. No cylinder ships in models/dev, so both are\r\n\t\t// squashed spheres; the brim is thin on X (the up-out-of-the-skull axis) and round across Y and Z.\r\n\t\tRegister( session, head, \u0022Hat\u0022,\r\n\t\t\ttunedFrame: \u0022citizen/head\u0022,\r\n\t\t\ttunedPosition: new Vector3( 15f, 1.901744f, 0.000369f ),\r\n\t\t\ttunedAngles: new Angles( 0f, 0f, 0f ),\r\n\t\t\ttunedScale: 1f,\r\n\t\t\tbodySeed: new Vector3( 2f, 0f, 6f ),\r\n\t\t\ttint: new Color( 0.42f, 0.86f, 1f ),\r\n\t\t\tparts: new[]\r\n\t\t\t{\r\n\t\t\t\tnew Part( \u0022models/dev/sphere.vmdl\u0022, new Vector3( 1.6f, 7f, 7f ), new Vector3( 0f, 0f, 0f ) ),\r\n\t\t\t\tnew Part( \u0022models/dev/sphere.vmdl\u0022, new Vector3( 2.6f, 4.2f, 4.2f ), new Vector3( 2f, 0f, 0f ) ),\r\n\t\t\t} );\r\n\r\n\t\t// Pack: the box was close, so this is only a proportion change, taller up the spine than it is deep\r\n\t\t// off the back. Kept chunky rather than slab-thin so it still reads as a pack under any axis order.\r\n\t\tRegister( session, back, \u0022Back Pack\u0022,\r\n\t\t\ttunedFrame: \u0022citizen/spine_2\u0022,\r\n\t\t\ttunedPosition: new Vector3( 1.625522f, -9.075111f, -0.00035f ),\r\n\t\t\ttunedAngles: new Angles( 0f, 0f, 0f ),\r\n\t\t\ttunedScale: 1f,\r\n\t\t\tbodySeed: new Vector3( -9f, 0f, 2f ),\r\n\t\t\ttint: new Color( 0.85f, 0.45f, 0.95f ),\r\n\t\t\tparts: new[]\r\n\t\t\t{\r\n\t\t\t\tnew Part( \u0022models/dev/box.vmdl\u0022, new Vector3( 14f, 7f, 11f ), new Vector3( 0f, 0f, 0f ) ),\r\n\t\t\t} );\r\n\t}\r\n\r\n\tCharacterAttachPoint BuildMount( GameObject citizen, string name, List\u003Cstring\u003E attachments, List\u003Cstring\u003E bones )\r\n\t{\r\n\t\tvar go = Scene.CreateObject();\r\n\t\tgo.Name = name;\r\n\t\tgo.SetParent( citizen, false );\r\n\t\tgo.LocalPosition = Vector3.Zero;\r\n\t\tgo.LocalRotation = Rotation.Identity;\r\n\r\n\t\tvar mount = go.Components.Create\u003CCharacterAttachPoint\u003E();\r\n\t\tmount.AttachmentNames = attachments;\r\n\t\tmount.BoneNames = bones;\r\n\t\tmount.CharacterName = \u0022citizen\u0022;\r\n\t\treturn mount;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Build one accessory under a mount and register it for tweaking. The root is a bare GameObject: it\r\n\t/// owns the offset the panel edits and a uniform scale that starts at 1, and nothing else. The parts\r\n\t/// hang under it carrying the models and the proportions.\r\n\t///\r\n\t/// The starting transform is applied later, once the mount resolves. See \u003Csee cref=\u0022SeedPending\u0022/\u003E.\r\n\t/// \u003C/summary\u003E\r\n\tvoid Register( TweakSession session, CharacterAttachPoint mount, string label,\r\n\t\tstring tunedFrame, Vector3 tunedPosition, Angles tunedAngles, float tunedScale, Vector3 bodySeed,\r\n\t\tColor tint, Part[] parts )\r\n\t{\r\n\t\tvar go = Scene.CreateObject();\r\n\t\tgo.Name = label;\r\n\t\tgo.SetParent( mount.GameObject, false );\r\n\t\tgo.LocalPosition = Vector3.Zero;\r\n\t\tgo.LocalRotation = Rotation.Identity;\r\n\t\tgo.LocalScale = Vector3.One;   // the panel\u0027s Scale row is a multiplier on the authored size, so 1 is \u0022as built\u0022\r\n\r\n\t\tfor ( int i = 0; i \u003C parts.Length; i\u002B\u002B )\r\n\t\t\tBuildPart( go, $\u0022{label} part {i \u002B 1}\u0022, parts[i], tint );\r\n\r\n\t\tsession.Add( new AttachedTweakTarget( go, mount, label ) );\r\n\t\t_pending.Add( new Pending( go, mount, tunedFrame, tunedPosition, tunedAngles, tunedScale, bodySeed ) );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// One primitive under an accessory root, sized in engine units.\r\n\t///\r\n\t/// The scale is the requested size divided by the model\u0027s OWN bounds, per axis, rather than a\r\n\t/// hand-tuned multiplier. That keeps the part table readable as real dimensions and survives the engine\r\n\t/// changing what a dev primitive measures: the shipped box is 50 units and the sphere is 64, and\r\n\t/// nothing here has to know that.\r\n\t/// \u003C/summary\u003E\r\n\tvoid BuildPart( GameObject parent, string name, Part part, Color tint )\r\n\t{\r\n\t\tvar go = Scene.CreateObject();\r\n\t\tgo.Name = name;\r\n\t\tgo.SetParent( parent, false );\r\n\t\tgo.LocalPosition = part.LocalPosition;\r\n\t\tgo.LocalRotation = Rotation.Identity;\r\n\r\n\t\tvar renderer = go.Components.Create\u003CModelRenderer\u003E();\r\n\t\tvar model = Model.Load( part.ModelPath );\r\n\t\tif ( model is null || model.IsError )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022[placement] demo part model \u0027{part.ModelPath}\u0027 did not load; \u0027{name}\u0027 will be invisible.\u0022 );\r\n\t\t\treturn;\r\n\t\t}\r\n\t\trenderer.Model = model;\r\n\r\n\t\tvar bounds = model.Bounds.Size;\r\n\t\tgo.LocalScale = new Vector3(\r\n\t\t\tbounds.x \u003E 0.001f ? part.SizeUnits.x / bounds.x : 1f,\r\n\t\t\tbounds.y \u003E 0.001f ? part.SizeUnits.y / bounds.y : 1f,\r\n\t\t\tbounds.z \u003E 0.001f ? part.SizeUnits.z / bounds.z : 1f );\r\n\r\n\t\t// The engine\u0027s models/dev primitives render as missing-material magenta unless a real material is\r\n\t\t// forced on, which would swallow the tint that tells the three accessories apart.\r\n\t\tvar mat = Material.Load( FallbackMaterial );\r\n\t\tif ( mat is not null ) renderer.MaterialOverride = mat;\r\n\t\trenderer.Tint = tint;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Apply each accessory\u0027s starting transform once its mount has resolved, then re-baseline it as the\r\n\t/// authored default so the panel\u0027s Reset comes back here.\r\n\t///\r\n\t/// Two paths, and which one runs depends on what the rig gave us:\r\n\t///\r\n\t///  - The mount landed on exactly the bone the demo values were measured against: apply them verbatim.\r\n\t///    These are real numbers, dialled in on the stock citizen, so the demo opens already fitted and the\r\n\t///    first thing you see is the finished result rather than three primitives in a heap.\r\n\t///  - Anything else, including the character-root fallback and any renamed or substituted bone: fall\r\n\t///    back to a seed expressed in the CHARACTER\u0027s frame (x forward, y left, z up) and let the engine\r\n\t///    convert it to local. Bone-local numbers are only valid on the rig they were measured on; reusing\r\n\t///    them elsewhere buries an accessory in the chest on one rig and flings it into orbit on the next.\r\n\t///    The character frame is not accurate, but it is always visible, which is all a fallback owes you.\r\n\t///\r\n\t/// The frame test requires a BONE mount, not just a matching name. An attachment that happens to be\r\n\t/// called \u0022head\u0022 is a different transform from the head bone, and the tuned numbers would be wrong on it.\r\n\t/// \u003C/summary\u003E\r\n\tvoid SeedPending()\r\n\t{\r\n\t\tif ( _pending.Count == 0 ) return;\r\n\r\n\t\tvar session = TweakSession.Instance;\r\n\t\tvar bodyRot = _citizen.IsValid() ? _citizen.WorldRotation : Rotation.Identity;\r\n\r\n\t\tfor ( int i = _pending.Count - 1; i \u003E= 0; i-- )\r\n\t\t{\r\n\t\t\tvar p = _pending[i];\r\n\t\t\tif ( !p.Accessory.IsValid() || !p.Mount.IsValid() ) { _pending.RemoveAt( i ); continue; }\r\n\t\t\tif ( p.Mount.Kind == CharacterAttachPoint.MountKind.Unresolved ) continue;\r\n\r\n\t\t\tbool tuned = p.Mount.Kind == CharacterAttachPoint.MountKind.Bone\r\n\t\t\t\t\u0026\u0026 p.Mount.FrameName == p.TunedFrame;\r\n\r\n\t\t\tif ( tuned )\r\n\t\t\t{\r\n\t\t\t\tp.Accessory.LocalPosition = p.TunedPosition;\r\n\t\t\t\tp.Accessory.LocalRotation = p.TunedAngles.ToRotation();\r\n\t\t\t\tp.Accessory.LocalScale = new Vector3( p.TunedScale, p.TunedScale, p.TunedScale );\r\n\t\t\t\tLog.Info( $\u0022[placement] \u0027{p.Accessory.Name}\u0027 seated at the tuned offset for {p.TunedFrame}\u0022 );\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tp.Accessory.WorldPosition = p.Mount.WorldPosition \u002B bodyRot * p.BodySeed;\r\n\t\t\t\tLog.Info( $\u0022[placement] \u0027{p.Accessory.Name}\u0027 mounted on {p.Mount.FrameName}, not the tuned \u0022\r\n\t\t\t\t\t\u002B $\u0022{p.TunedFrame}; using the generic character-frame seed instead. Fit it with P.\u0022 );\r\n\t\t\t}\r\n\r\n\t\t\tsession?.CaptureSeed( p.Accessory );\r\n\t\t\t_pending.RemoveAt( i );\r\n\t\t}\r\n\t}\r\n\r\n\t// ---- screen UI ----\r\n\r\n\tvoid BuildUi()\r\n\t{\r\n\t\t// One ScreenPanel per PanelComponent (the World Builder UI idiom). Built in code so the demo scene\r\n\t\t// needs no razor wiring.\r\n\t\tvar panelHost = Scene.CreateObject();\r\n\t\tpanelHost.Name = \u0022Placement UI\u0022;\r\n\t\tpanelHost.Components.Create\u003CScreenPanel\u003E();\r\n\r\n\t\t// Open on arrival. Fitting the accessories is what this scene is FOR, so making the visitor find the\r\n\t\t// key first is a toll booth on the way to the point. P and the header x still close it. The panel\r\n\t\t// reads this on its first update, after every OnStart in the frame, so setting it here always lands.\r\n\t\tvar panel = panelHost.Components.Create\u003CTweakPanel\u003E();\r\n\t\tpanel.OpenOnStart = true;\r\n\r\n\t\tvar hintHost = Scene.CreateObject();\r\n\t\thintHost.Name = \u0022Placement Hint\u0022;\r\n\t\thintHost.Components.Create\u003CScreenPanel\u003E();\r\n\t\thintHost.Components.Create\u003CDemoHintCard\u003E();\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.placement","Path":"Tweak/ITweakTarget.cs","FileName":"ITweakTarget.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337697,"Code":"using Sandbox;\r\n\r\nnamespace FieldGuide.Placement;\r\n\r\n/// \u003Csummary\u003E\r\n/// The seam the tweak panel edits. A target is a display name plus the GameObject whose LOCAL transform\r\n/// (position, rotation as pitch/yaw/roll, uniform scale) the panel nudges.\r\n///\r\n/// Only \u003Csee cref=\u0022DisplayName\u0022/\u003E and \u003Csee cref=\u0022Target\u0022/\u003E are required. Everything below is\r\n/// default-implemented, so an existing implementation keeps compiling and gets sensible behaviour:\r\n/// accessory-scale sliders, a \u0022local\u0022 frame label, a bake id derived from the display name, and the\r\n/// kit\u0027s standard bake line. Override the ones you care about.\r\n///\r\n/// Implement this to expose your own objects to the panel, or use the built-in \u003Csee cref=\u0022TweakTarget\u0022/\u003E\r\n/// wrapper around a plain GameObject.\r\n/// \u003C/summary\u003E\r\npublic interface ITweakTarget\r\n{\r\n\t/// \u003Csummary\u003EThe label shown on this target\u0027s tab in the panel.\u003C/summary\u003E\r\n\tstring DisplayName { get; }\r\n\r\n\t/// \u003Csummary\u003EThe object whose LocalPosition / LocalRotation / LocalScale the panel edits.\u003C/summary\u003E\r\n\tGameObject Target { get; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// What the edited offset is measured against: the parent this object hangs from, named in words your\r\n\t/// game code recognises. \u0022citizen/hold_R\u0022, \u0022citizen/spine_2\u0022, \u0022turret/muzzle\u0022. It shows on the panel\u0027s\r\n\t/// sub-line and goes into the export and the bake comment, so a pasted offset says what it is relative\r\n\t/// to. Defaults to \u003Csee cref=\u0022BakeFormat.LocalFrame\u0022/\u003E, which just means \u0022this object\u0027s parent\u0022.\r\n\t/// \u003C/summary\u003E\r\n\tstring FrameName =\u003E BakeFormat.LocalFrame;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The identifier the bake line names, e.g. \u0022hat\u0022 in \u003Cc\u003Enew PlacedOffset( \u0022hat\u0022, ... )\u003C/c\u003E. Defaults to\r\n\t/// a code-safe squashing of \u003Csee cref=\u0022DisplayName\u0022/\u003E, so \u0022Back Pack\u0022 bakes as \u0022back_pack\u0022.\r\n\t/// \u003C/summary\u003E\r\n\tstring BakeSymbol =\u003E BakeFormat.SymbolFrom( DisplayName );\r\n\r\n\t/// \u003Csummary\u003ESlider bounds and steps for this target\u0027s rows. Defaults to\r\n\t/// \u003Csee cref=\u0022TweakRanges.Accessory\u0022/\u003E; use \u003Csee cref=\u0022TweakRanges.Scene\u0022/\u003E for a placed prop.\u003C/summary\u003E\r\n\tTweakRanges Ranges =\u003E TweakRanges.Accessory;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Optional shaping hook for the paste-ready bake line. Return null (the default) for the kit\u0027s\r\n\t/// standard two-line form, or return your own text to bake straight into your project\u0027s real shape,\r\n\t/// e.g. \u003Cc\u003EItemMounts.HatOffset = new Vector3( ... );\u003C/c\u003E. The \u003Cparamref name=\u0022item\u0022/\u003E handed in\r\n\t/// already carries this target\u0027s live local transform, its \u003Csee cref=\u0022BakeSymbol\u0022/\u003E as the id, and its\r\n\t/// \u003Csee cref=\u0022FrameName\u0022/\u003E; format it with \u003Csee cref=\u0022BakeFormat.F(float)\u0022/\u003E to match the kit\u0027s\r\n\t/// invariant-culture float style.\r\n\t/// \u003C/summary\u003E\r\n\tstring FormatBakeLine( PlacedItem item ) =\u003E null;\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// A minimal \u003Csee cref=\u0022ITweakTarget\u0022/\u003E that wraps a GameObject, defaulting its label to the object\u0027s\r\n/// name. Convenience for the common case where a target is just \u0022this GameObject\u0022, with the optional\r\n/// interface members lifted to settable properties so a caller can fill them at registration.\r\n/// \u003C/summary\u003E\r\npublic sealed class TweakTarget : ITweakTarget\r\n{\r\n\tpublic string DisplayName { get; set; }\r\n\tpublic GameObject Target { get; set; }\r\n\r\n\t/// \u003Csummary\u003EWhat the offset is relative to, shown on the panel sub-line and written into the export.\r\n\t/// Leave null for the \u0022local\u0022 default.\u003C/summary\u003E\r\n\tpublic string Frame { get; set; }\r\n\r\n\t/// \u003Csummary\u003EThe id the bake line names. Leave null to derive it from \u003Csee cref=\u0022DisplayName\u0022/\u003E.\u003C/summary\u003E\r\n\tpublic string Symbol { get; set; }\r\n\r\n\t/// \u003Csummary\u003ESlider bounds and steps. Defaults to accessory scale.\u003C/summary\u003E\r\n\tpublic TweakRanges Ranges { get; set; } = TweakRanges.Accessory;\r\n\r\n\tstring ITweakTarget.FrameName =\u003E string.IsNullOrEmpty( Frame ) ? BakeFormat.LocalFrame : Frame;\r\n\tstring ITweakTarget.BakeSymbol =\u003E string.IsNullOrEmpty( Symbol ) ? BakeFormat.SymbolFrom( DisplayName ) : Symbol;\r\n\tTweakRanges ITweakTarget.Ranges =\u003E Ranges ?? TweakRanges.Accessory;\r\n\r\n\tpublic TweakTarget( GameObject go, string name = null )\r\n\t{\r\n\t\tTarget = go;\r\n\t\tDisplayName = string.IsNullOrEmpty( name ) ? (go?.Name ?? \u0022Object\u0022) : name;\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.placement","Path":"Code/Attach/CharacterAttachPoint.cs","FileName":"CharacterAttachPoint.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337697,"Code":"using Sandbox;\r\nusing System.Collections.Generic;\r\n\r\nnamespace FieldGuide.Placement;\r\n\r\n/// \u003Csummary\u003E\r\n/// A mount frame on a skinned character: this GameObject rides a named ATTACHMENT (a hand hold point,\r\n/// a hat point) or, failing that, a named BONE, so anything parented under it inherits that frame and\r\n/// can be positioned with a plain LocalPosition / LocalRotation.\r\n///\r\n/// This is the piece the accessory-fitting flow is built on. Put this component on an empty child of\r\n/// the character, list the attachment and bone names you want in preference order, then parent your\r\n/// model under it and register the model with the \u003Csee cref=\u0022TweakSession\u0022/\u003E. The offsets you drag in\r\n/// the panel are then genuinely relative to the hand or the spine, which is what\r\n/// \u003Csee cref=\u0022PlacedOffset\u0022/\u003E bakes and what your game code will reproduce.\r\n///\r\n/// Resolution is DEFERRED and RETRIED, not done once at start: a skinned model created this frame has no\r\n/// attachment objects yet, and reading a bone before the first pose evaluates gives the bind pose. The\r\n/// component keeps trying for \u003Csee cref=\u0022ResolveTimeout\u0022/\u003E seconds, then settles on the character root\r\n/// and says so in the console. A renamed or missing attachment therefore degrades to a visible,\r\n/// diagnosable fallback instead of a null reference.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Character Attach Point\u0022 )]\r\n[Category( \u0022Field Guide \u00B7 Placement\u0022 )]\r\n[Icon( \u0022accessibility\u0022 )]\r\npublic sealed class CharacterAttachPoint : Component\r\n{\r\n\t/// \u003Csummary\u003EHow the mount ended up anchored, once resolution settles.\u003C/summary\u003E\r\n\tpublic enum MountKind\r\n\t{\r\n\t\t/// \u003Csummary\u003EStill looking (the model may not have posed yet).\u003C/summary\u003E\r\n\t\tUnresolved,\r\n\t\t/// \u003Csummary\u003EParented to a model attachment object; the engine drives it.\u003C/summary\u003E\r\n\t\tAttachment,\r\n\t\t/// \u003Csummary\u003ERe-pinned to a bone transform every frame before render.\u003C/summary\u003E\r\n\t\tBone,\r\n\t\t/// \u003Csummary\u003ENothing resolved; sitting on the character root so the accessory is still visible.\u003C/summary\u003E\r\n\t\tRoot,\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe skinned model to mount against. Left null, the component searches its ancestors and\r\n\t/// then their descendants, which covers \u0022empty child of the character GameObject\u0022.\u003C/summary\u003E\r\n\t[Property] public SkinnedModelRenderer Renderer { get; set; }\r\n\r\n\t/// \u003Csummary\u003EAttachment names to try, in order. Casing varies between models, so list both forms\r\n\t/// (for example \u0022hold_R\u0022 then \u0022hold_r\u0022). Tried before \u003Csee cref=\u0022BoneNames\u0022/\u003E.\u003C/summary\u003E\r\n\t[Property] public List\u003Cstring\u003E AttachmentNames { get; set; } = new();\r\n\r\n\t/// \u003Csummary\u003EBone names to try if no attachment resolves. A bone mount is re-pinned every frame, so the\r\n\t/// accessory bobs and leans with the animation instead of sliding with the root.\u003C/summary\u003E\r\n\t[Property] public List\u003Cstring\u003E BoneNames { get; set; } = new();\r\n\r\n\t/// \u003Csummary\u003ELabel for the character in the frame string, e.g. \u0022citizen\u0022 gives \u0022citizen/hold_R\u0022. This is\r\n\t/// what ends up in the export and the bake comment, so name it the way your code talks about it.\u003C/summary\u003E\r\n\t[Property] public string CharacterName { get; set; } = \u0022character\u0022;\r\n\r\n\t/// \u003Csummary\u003ESeconds to keep retrying before giving up and falling back to the character root.\u003C/summary\u003E\r\n\t[Property] public float ResolveTimeout { get; set; } = 3f;\r\n\r\n\t/// \u003Csummary\u003EHow this mount is anchored right now.\u003C/summary\u003E\r\n\tpublic MountKind Kind { get; private set; } = MountKind.Unresolved;\r\n\r\n\t/// \u003Csummary\u003EThe attachment or bone name that actually resolved, or \u0022root\u0022 on the fallback.\u003C/summary\u003E\r\n\tpublic string ResolvedName { get; private set; } = \u0022root\u0022;\r\n\r\n\t/// \u003Csummary\u003EThe frame string to hand \u003Csee cref=\u0022TweakSession.Add(GameObject, string, string, TweakRanges)\u0022/\u003E:\r\n\t/// \u0022citizen/hold_R\u0022, \u0022citizen/spine_2\u0022, \u0022citizen/root\u0022. Safe to read before resolution settles; it just\r\n\t/// reports the fallback until then.\u003C/summary\u003E\r\n\tpublic string FrameName =\u003E $\u0022{CharacterName}/{ResolvedName}\u0022;\r\n\r\n\tfloat _elapsed;\r\n\tbool _gaveUp;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\tRenderer ??= Components.GetInAncestorsOrSelf\u003CSkinnedModelRenderer\u003E()\r\n\t\t\t?? Components.GetInDescendantsOrSelf\u003CSkinnedModelRenderer\u003E( false );\r\n\t\tTryResolve();\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( Kind is MountKind.Attachment or MountKind.Bone ) return;\r\n\r\n\t\t_elapsed \u002B= Time.Delta;\r\n\t\tif ( TryResolve() ) return;\r\n\r\n\t\tif ( _gaveUp || _elapsed \u003C ResolveTimeout ) return;\r\n\r\n\t\t_gaveUp = true;\r\n\t\tKind = MountKind.Root;\r\n\t\tResolvedName = \u0022root\u0022;\r\n\t\tLog.Warning( $\u0022[placement] attach point \u0027{GameObject?.Name}\u0027 resolved no attachment {Names( AttachmentNames )} \u0022\r\n\t\t\t\u002B $\u0022and no bone {Names( BoneNames )} on the model; falling back to the character root.\u0022 );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EA bone mount is re-pinned here rather than in OnUpdate: PreRender runs after the pose is\r\n\t/// evaluated, so the accessory sits on the bone position that will actually be drawn this frame.\u003C/summary\u003E\r\n\tprotected override void OnPreRender()\r\n\t{\r\n\t\tif ( Kind != MountKind.Bone || !Renderer.IsValid() ) return;\r\n\t\tif ( !Renderer.TryGetBoneTransform( ResolvedName, out Transform t ) ) return;\r\n\t\tWorldPosition = t.Position;\r\n\t\tWorldRotation = t.Rotation;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EOne resolution attempt. Attachments win (the engine parents and drives them for free);\r\n\t/// bones are the fallback that still tracks the animation. Returns true once anchored.\u003C/summary\u003E\r\n\tbool TryResolve()\r\n\t{\r\n\t\tif ( !Renderer.IsValid() ) return false;\r\n\r\n\t\tforeach ( var name in AttachmentNames )\r\n\t\t{\r\n\t\t\tif ( string.IsNullOrEmpty( name ) ) continue;\r\n\t\t\tvar mount = Renderer.GetAttachmentObject( name );\r\n\t\t\tif ( mount is null || !mount.IsValid() ) continue;\r\n\r\n\t\t\tGameObject.SetParent( mount, false );\r\n\t\t\tGameObject.LocalPosition = Vector3.Zero;\r\n\t\t\tGameObject.LocalRotation = Rotation.Identity;\r\n\t\t\tKind = MountKind.Attachment;\r\n\t\t\tResolvedName = name;\r\n\t\t\tLog.Info( $\u0022[placement] attach point \u0027{GameObject.Name}\u0027 mounted on attachment {FrameName}\u0022 );\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\tforeach ( var name in BoneNames )\r\n\t\t{\r\n\t\t\tif ( string.IsNullOrEmpty( name ) ) continue;\r\n\t\t\tif ( !Renderer.TryGetBoneTransform( name, out Transform t ) ) continue;\r\n\r\n\t\t\tWorldPosition = t.Position;\r\n\t\t\tWorldRotation = t.Rotation;\r\n\t\t\tKind = MountKind.Bone;\r\n\t\t\tResolvedName = name;\r\n\t\t\tLog.Info( $\u0022[placement] attach point \u0027{GameObject.Name}\u0027 mounted on bone {FrameName} (re-pinned each frame)\u0022 );\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\tstatic string Names( List\u003Cstring\u003E names )\r\n\t\t=\u003E names is null || names.Count == 0 ? \u0022(none listed)\u0022 : \u0022[\u0022 \u002B string.Join( \u0022, \u0022, names ) \u002B \u0022]\u0022;\r\n}\r\n"},{"Ident":"fieldguide.placement","Path":"Code/Tweak/TweakRanges.cs","FileName":"TweakRanges.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337697,"Code":"namespace FieldGuide.Placement;\r\n\r\n/// \u003Csummary\u003E\r\n/// Slider bounds and step sizes for one tweak target\u0027s rows, in ENGINE UNITS and degrees.\r\n///\r\n/// Why this is per-target and not a constant: fitting a hat to a head and laying out a building are the\r\n/// same three sliders over wildly different ranges. A position row spanning \u002B/-1024 units at step 1 (the\r\n/// kit\u0027s original fixed range) cannot seat an accessory at all: one pixel of drag is several units, and\r\n/// the value you want lives in the first half-percent of the track. The accessory default below spans\r\n/// \u002B/-48 units at step 0.25, so the xfine step (\u00F710) reaches 0.025 units and a full drag still covers a\r\n/// character-sized volume.\r\n/// \u003C/summary\u003E\r\npublic sealed record TweakRanges(\r\n\tfloat PositionRange = 48f,\r\n\tfloat PositionStep = 0.25f,\r\n\tfloat RotationStep = 1f,\r\n\tfloat ScaleMin = 0.05f,\r\n\tfloat ScaleMax = 4f,\r\n\tfloat ScaleStep = 0.05f )\r\n{\r\n\t/// \u003Csummary\u003EThe default: character-accessory scale. \u002B/-48 units at 0.25 (xfine reaches 0.025), rotation\r\n\t/// at 1 degree, scale 0.05 to 4. This is what an unconfigured target gets.\u003C/summary\u003E\r\n\tpublic static readonly TweakRanges Accessory = new();\r\n\r\n\t/// \u003Csummary\u003EScene-authoring scale, for tweaking a placed prop rather than a worn accessory:\r\n\t/// \u002B/-1024 units at step 1, scale up to 8.\u003C/summary\u003E\r\n\tpublic static readonly TweakRanges Scene = new( PositionRange: 1024f, PositionStep: 1f, ScaleMax: 8f );\r\n\r\n\t/// \u003Csummary\u003ERoom-scale, between the two: \u002B/-256 units at 0.5.\u003C/summary\u003E\r\n\tpublic static readonly TweakRanges Prop = new( PositionRange: 256f, PositionStep: 0.5f );\r\n}\r\n"},{"Ident":"fieldguide.placement","Path":"Export/BakeFormat.cs","FileName":"BakeFormat.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337697,"Code":"using System.Globalization;\r\nusing System.Text;\r\n\r\nnamespace FieldGuide.Placement;\r\n\r\n/// \u003Csummary\u003E\r\n/// The text rules every export path shares: how a float becomes a C# literal, how a string is escaped,\r\n/// and how a display name becomes a code-safe id. One home for them so the JSON file, the C# snippet,\r\n/// and the panel\u0027s Copy button cannot drift apart in formatting.\r\n/// \u003C/summary\u003E\r\npublic static class BakeFormat\r\n{\r\n\t/// \u003Csummary\u003EThe \u003Csee cref=\u0022ITweakTarget.FrameName\u0022/\u003E default: \u0022this object\u0027s parent\u0022, nothing more\r\n\t/// specific known.\u003C/summary\u003E\r\n\tpublic const string LocalFrame = \u0022local\u0022;\r\n\r\n\t/// \u003Csummary\u003EA float as a C# literal body: up to six decimals, invariant culture so a decimal never\r\n\t/// localises to a comma and breaks the pasted code. Callers append the \u0022f\u0022 suffix.\u003C/summary\u003E\r\n\tpublic static string F( float v ) =\u003E v.ToString( \u00220.######\u0022, CultureInfo.InvariantCulture );\r\n\r\n\t/// \u003Csummary\u003EEscape a string for a C# / JSON double-quoted literal.\u003C/summary\u003E\r\n\tpublic static string Escape( string s ) =\u003E (s ?? \u0022\u0022).Replace( \u0022\\\\\u0022, \u0022\\\\\\\\\u0022 ).Replace( \u0022\\\u0022\u0022, \u0022\\\\\\\u0022\u0022 );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Squash a display name into a code-safe id: lowercase, non-alphanumerics collapsed to single\r\n\t/// underscores, no leading or trailing underscore. \u0022Back Pack\u0022 becomes \u0022back_pack\u0022, \u0022Hat (left)\u0022\r\n\t/// becomes \u0022hat_left\u0022. Falls back to \u0022item\u0022 for an empty or fully-stripped name.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static string SymbolFrom( string displayName )\r\n\t{\r\n\t\tif ( string.IsNullOrWhiteSpace( displayName ) ) return \u0022item\u0022;\r\n\r\n\t\tvar sb = new StringBuilder( displayName.Length );\r\n\t\tbool pendingUnderscore = false;\r\n\t\tforeach ( char c in displayName )\r\n\t\t{\r\n\t\t\tif ( char.IsLetterOrDigit( c ) )\r\n\t\t\t{\r\n\t\t\t\tif ( pendingUnderscore \u0026\u0026 sb.Length \u003E 0 ) sb.Append( \u0027_\u0027 );\r\n\t\t\t\tpendingUnderscore = false;\r\n\t\t\t\tsb.Append( char.ToLowerInvariant( c ) );\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tpendingUnderscore = true;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn sb.Length == 0 ? \u0022item\u0022 : sb.ToString();\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.placement","Path":"Code/Demo/DemoHintCard.razor.scss","FileName":"DemoHintCard.razor.scss","PackageType":"library","CodeKind":"Game","AssetVersionId":337697,"Code":"// Demo hint card. Same visual language as the tweak panel: 3 grouped font sizes (13 / 11 / 10), no\r\n// letter-spacing, borders via border-width \u002B border-color only (never border-style, which aborts the\r\n// whole stylesheet), no blurred box-shadow on rounded elements. Root is pointer-events:none so only the\r\n// card takes clicks. Every text colour sits above the contrast floor: instructions are never dim gray.\r\n//\r\n// The close control is a 42px square, matching the tweak panel. No ESC badge anywhere.\r\n\r\nDemoHintCard {\r\n\tposition: absolute;\r\n\ttop: 0;\r\n\tleft: 0;\r\n\twidth: 100%;\r\n\theight: 100%;\r\n\tpointer-events: none;\r\n\r\n\t.hc-card {\r\n\t\tposition: absolute;\r\n\t\ttop: 80px;\r\n\t\tleft: 24px;\r\n\t\twidth: 340px;\r\n\t\tflex-direction: column;\r\n\t\tpointer-events: all;\r\n\t\tbackground-color: rgba( 14, 16, 20, 0.94 );\r\n\t\tborder-width: 1px;\r\n\t\tborder-color: rgba( 90, 220, 255, 0.55 );\r\n\t\tborder-radius: 8px;\r\n\t\tpadding: 12px;\r\n\t\tfont-family: \u0022Roboto Mono\u0022, monospace;\r\n\t\tcolor: #eaf6ff;\r\n\t}\r\n\r\n\t.hc-hdr {\r\n\t\tflex-direction: row;\r\n\t\tjustify-content: space-between;\r\n\t\talign-items: center;\r\n\t\tmargin-bottom: 6px;\r\n\t}\r\n\t.hc-title {\r\n\t\tfont-size: 13px;\r\n\t\tcolor: #7fe6ff;\r\n\t}\r\n\t.hc-x {\r\n\t\tfont-size: 13px;\r\n\t\tcolor: #ffffff;\r\n\t\twidth: 42px;\r\n\t\theight: 42px;\r\n\t\tjustify-content: center;\r\n\t\talign-items: center;\r\n\t\tborder-radius: 6px;\r\n\t\tborder-width: 1px;\r\n\t\tborder-color: rgba( 90, 220, 255, 0.45 );\r\n\t\tbackground-color: rgba( 255, 255, 255, 0.06 );\r\n\t\tpointer-events: all;\r\n\t\ttransition: all 0.1s ease;\r\n\t\t\u0026:hover {\r\n\t\t\tbackground-color: rgba( 90, 220, 255, 0.28 );\r\n\t\t\tborder-color: rgba( 90, 220, 255, 0.9 );\r\n\t\t}\r\n\t}\r\n\r\n\t.hc-lede {\r\n\t\tfont-size: 11px;\r\n\t\tcolor: #ffffff;\r\n\t\tmargin-bottom: 10px;\r\n\t}\r\n\r\n\t.hc-row {\r\n\t\tflex-direction: row;\r\n\t\talign-items: center;\r\n\t\tmargin-bottom: 5px;\r\n\t}\r\n\t.hc-key {\r\n\t\tfont-size: 10px;\r\n\t\tcolor: #ffffff;\r\n\t\tfont-weight: 700;\r\n\t\tbackground-color: rgba( 90, 220, 255, 0.18 );\r\n\t\tborder-radius: 4px;\r\n\t\tpadding: 3px 7px;\r\n\t\tmargin-right: 8px;\r\n\t\twidth: 96px;\r\n\t\tjustify-content: center;\r\n\t\talign-items: center;\r\n\t}\r\n\t.hc-what {\r\n\t\tfont-size: 11px;\r\n\t\tcolor: #eaf6ff;\r\n\t\tflex-grow: 1;\r\n\t}\r\n\r\n\t.hc-foot {\r\n\t\tfont-size: 10px;\r\n\t\tcolor: #cfe0ea;\r\n\t\tmargin-top: 8px;\r\n\t\tborder-top-width: 1px;\r\n\t\tborder-top-color: rgba( 255, 255, 255, 0.12 );\r\n\t\tpadding-top: 8px;\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.placement","Path":".obj/__compiler_extra.cs","FileName":"__compiler_extra.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337697,"Code":"global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonTitle\u0022, \u0022Placement Kit\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonIdent\u0022, \u0022placement\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022OrgIdent\u0022, \u0022fieldguide\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022Ident\u0022, \u0022fieldguide.placement\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineVersion\u0022, \u002228\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineMinorVersion\u0022, \u00221\u0022 )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \u0022.NETCoreApp,Version=v9.0\u0022, FrameworkDisplayName = \u0022.NET 9.0\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022CompileTime\u0022, \u00222026-07-31T00:57:51.8271982Z\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\u00220.0.121.0\u0022)]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\u00220.0.121.0\u0022)]"},{"Ident":"fieldguide.placement","Path":"Attach/CharacterAttachPoint.cs","FileName":"CharacterAttachPoint.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337697,"Code":"using Sandbox;\r\nusing System.Collections.Generic;\r\n\r\nnamespace FieldGuide.Placement;\r\n\r\n/// \u003Csummary\u003E\r\n/// A mount frame on a skinned character: this GameObject rides a named ATTACHMENT (a hand hold point,\r\n/// a hat point) or, failing that, a named BONE, so anything parented under it inherits that frame and\r\n/// can be positioned with a plain LocalPosition / LocalRotation.\r\n///\r\n/// This is the piece the accessory-fitting flow is built on. Put this component on an empty child of\r\n/// the character, list the attachment and bone names you want in preference order, then parent your\r\n/// model under it and register the model with the \u003Csee cref=\u0022TweakSession\u0022/\u003E. The offsets you drag in\r\n/// the panel are then genuinely relative to the hand or the spine, which is what\r\n/// \u003Csee cref=\u0022PlacedOffset\u0022/\u003E bakes and what your game code will reproduce.\r\n///\r\n/// Resolution is DEFERRED and RETRIED, not done once at start: a skinned model created this frame has no\r\n/// attachment objects yet, and reading a bone before the first pose evaluates gives the bind pose. The\r\n/// component keeps trying for \u003Csee cref=\u0022ResolveTimeout\u0022/\u003E seconds, then settles on the character root\r\n/// and says so in the console. A renamed or missing attachment therefore degrades to a visible,\r\n/// diagnosable fallback instead of a null reference.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Character Attach Point\u0022 )]\r\n[Category( \u0022Field Guide \u00B7 Placement\u0022 )]\r\n[Icon( \u0022accessibility\u0022 )]\r\npublic sealed class CharacterAttachPoint : Component\r\n{\r\n\t/// \u003Csummary\u003EHow the mount ended up anchored, once resolution settles.\u003C/summary\u003E\r\n\tpublic enum MountKind\r\n\t{\r\n\t\t/// \u003Csummary\u003EStill looking (the model may not have posed yet).\u003C/summary\u003E\r\n\t\tUnresolved,\r\n\t\t/// \u003Csummary\u003EParented to a model attachment object; the engine drives it.\u003C/summary\u003E\r\n\t\tAttachment,\r\n\t\t/// \u003Csummary\u003ERe-pinned to a bone transform every frame before render.\u003C/summary\u003E\r\n\t\tBone,\r\n\t\t/// \u003Csummary\u003ENothing resolved; sitting on the character root so the accessory is still visible.\u003C/summary\u003E\r\n\t\tRoot,\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe skinned model to mount against. Left null, the component searches its ancestors and\r\n\t/// then their descendants, which covers \u0022empty child of the character GameObject\u0022.\u003C/summary\u003E\r\n\t[Property] public SkinnedModelRenderer Renderer { get; set; }\r\n\r\n\t/// \u003Csummary\u003EAttachment names to try, in order. Casing varies between models, so list both forms\r\n\t/// (for example \u0022hold_R\u0022 then \u0022hold_r\u0022). Tried before \u003Csee cref=\u0022BoneNames\u0022/\u003E.\u003C/summary\u003E\r\n\t[Property] public List\u003Cstring\u003E AttachmentNames { get; set; } = new();\r\n\r\n\t/// \u003Csummary\u003EBone names to try if no attachment resolves. A bone mount is re-pinned every frame, so the\r\n\t/// accessory bobs and leans with the animation instead of sliding with the root.\u003C/summary\u003E\r\n\t[Property] public List\u003Cstring\u003E BoneNames { get; set; } = new();\r\n\r\n\t/// \u003Csummary\u003ELabel for the character in the frame string, e.g. \u0022citizen\u0022 gives \u0022citizen/hold_R\u0022. This is\r\n\t/// what ends up in the export and the bake comment, so name it the way your code talks about it.\u003C/summary\u003E\r\n\t[Property] public string CharacterName { get; set; } = \u0022character\u0022;\r\n\r\n\t/// \u003Csummary\u003ESeconds to keep retrying before giving up and falling back to the character root.\u003C/summary\u003E\r\n\t[Property] public float ResolveTimeout { get; set; } = 3f;\r\n\r\n\t/// \u003Csummary\u003EHow this mount is anchored right now.\u003C/summary\u003E\r\n\tpublic MountKind Kind { get; private set; } = MountKind.Unresolved;\r\n\r\n\t/// \u003Csummary\u003EThe attachment or bone name that actually resolved, or \u0022root\u0022 on the fallback.\u003C/summary\u003E\r\n\tpublic string ResolvedName { get; private set; } = \u0022root\u0022;\r\n\r\n\t/// \u003Csummary\u003EThe frame string to hand \u003Csee cref=\u0022TweakSession.Add(GameObject, string, string, TweakRanges)\u0022/\u003E:\r\n\t/// \u0022citizen/hold_R\u0022, \u0022citizen/spine_2\u0022, \u0022citizen/root\u0022. Safe to read before resolution settles; it just\r\n\t/// reports the fallback until then.\u003C/summary\u003E\r\n\tpublic string FrameName =\u003E $\u0022{CharacterName}/{ResolvedName}\u0022;\r\n\r\n\tfloat _elapsed;\r\n\tbool _gaveUp;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\tRenderer ??= Components.GetInAncestorsOrSelf\u003CSkinnedModelRenderer\u003E()\r\n\t\t\t?? Components.GetInDescendantsOrSelf\u003CSkinnedModelRenderer\u003E( false );\r\n\t\tTryResolve();\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( Kind is MountKind.Attachment or MountKind.Bone ) return;\r\n\r\n\t\t_elapsed \u002B= Time.Delta;\r\n\t\tif ( TryResolve() ) return;\r\n\r\n\t\tif ( _gaveUp || _elapsed \u003C ResolveTimeout ) return;\r\n\r\n\t\t_gaveUp = true;\r\n\t\tKind = MountKind.Root;\r\n\t\tResolvedName = \u0022root\u0022;\r\n\t\tLog.Warning( $\u0022[placement] attach point \u0027{GameObject?.Name}\u0027 resolved no attachment {Names( AttachmentNames )} \u0022\r\n\t\t\t\u002B $\u0022and no bone {Names( BoneNames )} on the model; falling back to the character root.\u0022 );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EA bone mount is re-pinned here rather than in OnUpdate: PreRender runs after the pose is\r\n\t/// evaluated, so the accessory sits on the bone position that will actually be drawn this frame.\u003C/summary\u003E\r\n\tprotected override void OnPreRender()\r\n\t{\r\n\t\tif ( Kind != MountKind.Bone || !Renderer.IsValid() ) return;\r\n\t\tif ( !Renderer.TryGetBoneTransform( ResolvedName, out Transform t ) ) return;\r\n\t\tWorldPosition = t.Position;\r\n\t\tWorldRotation = t.Rotation;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EOne resolution attempt. Attachments win (the engine parents and drives them for free);\r\n\t/// bones are the fallback that still tracks the animation. Returns true once anchored.\u003C/summary\u003E\r\n\tbool TryResolve()\r\n\t{\r\n\t\tif ( !Renderer.IsValid() ) return false;\r\n\r\n\t\tforeach ( var name in AttachmentNames )\r\n\t\t{\r\n\t\t\tif ( string.IsNullOrEmpty( name ) ) continue;\r\n\t\t\tvar mount = Renderer.GetAttachmentObject( name );\r\n\t\t\tif ( mount is null || !mount.IsValid() ) continue;\r\n\r\n\t\t\tGameObject.SetParent( mount, false );\r\n\t\t\tGameObject.LocalPosition = Vector3.Zero;\r\n\t\t\tGameObject.LocalRotation = Rotation.Identity;\r\n\t\t\tKind = MountKind.Attachment;\r\n\t\t\tResolvedName = name;\r\n\t\t\tLog.Info( $\u0022[placement] attach point \u0027{GameObject.Name}\u0027 mounted on attachment {FrameName}\u0022 );\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\tforeach ( var name in BoneNames )\r\n\t\t{\r\n\t\t\tif ( string.IsNullOrEmpty( name ) ) continue;\r\n\t\t\tif ( !Renderer.TryGetBoneTransform( name, out Transform t ) ) continue;\r\n\r\n\t\t\tWorldPosition = t.Position;\r\n\t\t\tWorldRotation = t.Rotation;\r\n\t\t\tKind = MountKind.Bone;\r\n\t\t\tResolvedName = name;\r\n\t\t\tLog.Info( $\u0022[placement] attach point \u0027{GameObject.Name}\u0027 mounted on bone {FrameName} (re-pinned each frame)\u0022 );\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\tstatic string Names( List\u003Cstring\u003E names )\r\n\t\t=\u003E names is null || names.Count == 0 ? \u0022(none listed)\u0022 : \u0022[\u0022 \u002B string.Join( \u0022, \u0022, names ) \u002B \u0022]\u0022;\r\n}\r\n"},{"Ident":"fieldguide.placement","Path":"Code/Placement/PlacementCatalog.cs","FileName":"PlacementCatalog.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337697,"Code":"using Sandbox;\r\nusing System;\r\nusing System.Collections.Generic;\r\n\r\nnamespace FieldGuide.Placement;\r\n\r\n/// \u003Csummary\u003E\r\n/// The consumer-supplied set of placeable things, plus the validity seam the ghost uses to tint valid\r\n/// vs invalid spots. Populate \u003Csee cref=\u0022Entries\u0022/\u003E in code (e.g. from a bootstrap) and, optionally, set\r\n/// \u003Csee cref=\u0022ValidityCheck\u0022/\u003E to gate where placement is allowed (default: anywhere the aim ray hits).\r\n/// Publishes itself through a static \u003Csee cref=\u0022Instance\u0022/\u003E so \u003Csee cref=\u0022GhostPlacer\u0022/\u003E needs no handle.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Placement Catalog\u0022 )]\r\n[Category( \u0022Field Guide \u00B7 Placement\u0022 )]\r\n[Icon( \u0022category\u0022 )]\r\npublic sealed class PlacementCatalog : Component\r\n{\r\n\tpublic static PlacementCatalog Instance { get; private set; }\r\n\r\n\t/// \u003Csummary\u003EThe placeable entries, in cycle order. Filled by the consumer at runtime.\u003C/summary\u003E\r\n\tpublic List\u003CPlaceableEntry\u003E Entries { get; } = new();\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Seam: return true if an object may be placed at the given world position and rotation. Null means\r\n\t/// \u0022always valid\u0022 (the default). The ghost tints green when this returns true, red when false, and a\r\n\t/// click only places on a valid spot.\r\n\t/// \u003C/summary\u003E\r\n\tpublic Func\u003CVector3, Rotation, bool\u003E ValidityCheck { get; set; }\r\n\r\n\tpublic bool IsValidSpot( Vector3 worldPos, Rotation worldRot )\r\n\t\t=\u003E ValidityCheck is null || ValidityCheck( worldPos, worldRot );\r\n\r\n\tprotected override void OnEnabled() =\u003E Instance = this;\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tif ( Instance == this ) Instance = null;\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.placement","Path":"Code/Tweak/TweakSession.cs","FileName":"TweakSession.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337697,"Code":"using Sandbox;\r\nusing System.Collections.Generic;\r\n\r\nnamespace FieldGuide.Placement;\r\n\r\n/// \u003Csummary\u003E\r\n/// Holds the list of objects the \u003Csee cref=\u0022TweakPanel\u0022/\u003E can edit, and publishes itself through a\r\n/// static \u003Csee cref=\u0022Instance\u0022/\u003E so the panel needs no component handle. Two ways to fill it:\r\n///\r\n///  1. Wire GameObjects into \u003Csee cref=\u0022Objects\u0022/\u003E in the scene inspector (the simple path).\r\n///  2. Call \u003Csee cref=\u0022Add(ITweakTarget)\u0022/\u003E / \u003Csee cref=\u0022Add(GameObject, string)\u0022/\u003E at runtime for\r\n///     code-supplied or custom targets. This is the path the accessory-fitting flow uses: parent your\r\n///     model to a character mount, then register it with a frame name so the bake says what the offset\r\n///     is relative to.\r\n///\r\n/// \u003Csee cref=\u0022Targets\u0022/\u003E merges both into the effective list the panel tabs across.\r\n///\r\n/// The session also remembers each target\u0027s AUTHORED transform, captured the moment it is registered, so\r\n/// the panel\u0027s Reset restores what you started with rather than slamming the object to zero (which for a\r\n/// hand-mounted accessory means \u0022collapsed into the wrist\u0022, never what you wanted).\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Tweak Session\u0022 )]\r\n[Category( \u0022Field Guide \u00B7 Placement\u0022 )]\r\n[Icon( \u0022tune\u0022 )]\r\npublic sealed class TweakSession : Component\r\n{\r\n\tpublic static TweakSession Instance { get; private set; }\r\n\r\n\t/// \u003Csummary\u003EScene-wired targets: drop GameObjects here in the inspector to make them tweakable.\u003C/summary\u003E\r\n\t[Property] public List\u003CGameObject\u003E Objects { get; set; } = new();\r\n\r\n\treadonly List\u003CITweakTarget\u003E _custom = new();\r\n\r\n\t/// \u003Csummary\u003EThe authored local transform of each registered target, captured at registration. Keyed by\r\n\t/// GameObject so a custom ITweakTarget and a wrapped GameObject share one entry.\u003C/summary\u003E\r\n\treadonly Dictionary\u003CGameObject, Seed\u003E _seeds = new();\r\n\r\n\treadonly record struct Seed( Vector3 Position, Rotation Rotation, Vector3 Scale );\r\n\r\n\t/// \u003Csummary\u003EThe effective, de-duplicated target list the panel tabs across: code-supplied targets\r\n\t/// first, then the scene-wired \u003Csee cref=\u0022Objects\u0022/\u003E (skipping any invalid or already-listed ones).\u003C/summary\u003E\r\n\tpublic IReadOnlyList\u003CITweakTarget\u003E Targets\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar list = new List\u003CITweakTarget\u003E();\r\n\t\t\tvar seen = new HashSet\u003CGameObject\u003E();\r\n\t\t\tforeach ( var t in _custom )\r\n\t\t\t{\r\n\t\t\t\tif ( t?.Target is null || !t.Target.IsValid() || !seen.Add( t.Target ) ) continue;\r\n\t\t\t\tlist.Add( t );\r\n\t\t\t}\r\n\t\t\tforeach ( var go in Objects )\r\n\t\t\t{\r\n\t\t\t\tif ( go is null || !go.IsValid() || !seen.Add( go ) ) continue;\r\n\t\t\t\tCaptureSeedIfNew( go );   // an object dropped into the list at runtime still gets an authored baseline\r\n\t\t\t\tlist.Add( new TweakTarget( go ) );\r\n\t\t\t}\r\n\t\t\treturn list;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ERegister a custom target (your own \u003Csee cref=\u0022ITweakTarget\u0022/\u003E implementation).\u003C/summary\u003E\r\n\tpublic void Add( ITweakTarget target )\r\n\t{\r\n\t\tif ( target?.Target is null ) return;\r\n\t\t_custom.Add( target );\r\n\t\tCaptureSeedIfNew( target.Target );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ERegister a GameObject as a target, wrapped in a \u003Csee cref=\u0022TweakTarget\u0022/\u003E.\u003C/summary\u003E\r\n\tpublic void Add( GameObject go, string name = null )\r\n\t{\r\n\t\tif ( go is null ) return;\r\n\t\t_custom.Add( new TweakTarget( go, name ) );\r\n\t\tCaptureSeedIfNew( go );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ERegister a GameObject with the frame its offset is measured against and, optionally, the\r\n\t/// slider ranges its rows should use. The frame name shows on the panel sub-line and lands in the\r\n\t/// export and the bake comment, which is the whole reason a baked offset is readable later.\u003C/summary\u003E\r\n\tpublic TweakTarget Add( GameObject go, string name, string frame, TweakRanges ranges = null )\r\n\t{\r\n\t\tif ( go is null ) return null;\r\n\t\tvar t = new TweakTarget( go, name ) { Frame = frame, Ranges = ranges ?? TweakRanges.Accessory };\r\n\t\t_custom.Add( t );\r\n\t\tCaptureSeedIfNew( go );\r\n\t\treturn t;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EDrop a previously-registered code-supplied target (does not affect scene-wired Objects).\u003C/summary\u003E\r\n\tpublic void Remove( GameObject go )\r\n\t{\r\n\t\t_custom.RemoveAll( t =\u003E t.Target == go );\r\n\t\t_seeds.Remove( go );\r\n\t}\r\n\r\n\t// ---- authored-default baseline ----\r\n\r\n\t/// \u003Csummary\u003ERecord this object\u0027s CURRENT local transform as its authored default, overwriting any\r\n\t/// earlier capture. Call it after you finish positioning an object in code if you want Reset to come\r\n\t/// back to that pose rather than the one it had at registration.\u003C/summary\u003E\r\n\tpublic void CaptureSeed( GameObject go )\r\n\t{\r\n\t\tif ( go is null || !go.IsValid() ) return;\r\n\t\t_seeds[go] = new Seed( go.LocalPosition, go.LocalRotation, go.LocalScale );\r\n\t}\r\n\r\n\tvoid CaptureSeedIfNew( GameObject go )\r\n\t{\r\n\t\tif ( go is null || !go.IsValid() || _seeds.ContainsKey( go ) ) return;\r\n\t\t_seeds[go] = new Seed( go.LocalPosition, go.LocalRotation, go.LocalScale );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ERestore a target to the transform captured when it was registered. Returns false if the\r\n\t/// object is invalid or was never registered here, so a caller can fall back if it wants to.\u003C/summary\u003E\r\n\tpublic bool ResetToSeed( GameObject go )\r\n\t{\r\n\t\tif ( go is null || !go.IsValid() || !_seeds.TryGetValue( go, out var seed ) ) return false;\r\n\t\tgo.LocalPosition = seed.Position;\r\n\t\tgo.LocalRotation = seed.Rotation;\r\n\t\tgo.LocalScale = seed.Scale;\r\n\t\treturn true;\r\n\t}\r\n\r\n\tprotected override void OnEnabled() =\u003E Instance = this;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\t// Baseline the scene-wired objects at start, so their authored inspector transform is what Reset\r\n\t\t// comes back to even if something moves them before the panel is first opened.\r\n\t\tforeach ( var go in Objects )\r\n\t\t\tCaptureSeedIfNew( go );\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tif ( Instance == this ) Instance = null;\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.placement","Path":"Placement/PlaceableEntry.cs","FileName":"PlaceableEntry.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337697,"Code":"using Sandbox;\r\n\r\nnamespace FieldGuide.Placement;\r\n\r\n/// \u003Csummary\u003E\r\n/// One entry in a \u003Csee cref=\u0022PlacementCatalog\u0022/\u003E: an id, a display name, and what to spawn. Supply\r\n/// either a \u003Csee cref=\u0022ModelPath\u0022/\u003E (the object is built as a GameObject \u002B ModelRenderer, the light\r\n/// path the ghost preview also uses) or a \u003Csee cref=\u0022Prefab\u0022/\u003E template GameObject (cloned on place,\r\n/// carrying all its components). If both are set, \u003Csee cref=\u0022Prefab\u0022/\u003E wins for the placed object.\r\n///\r\n/// \u003Csee cref=\u0022Id\u0022/\u003E is the stable key written into the export (JSON/C# snippet), so keep it terse and\r\n/// unique within a catalog (e.g. \u0022box\u0022, \u0022sphere\u0022, \u0022crate_a\u0022).\r\n/// \u003C/summary\u003E\r\npublic sealed class PlaceableEntry\r\n{\r\n\t/// \u003Csummary\u003EStable key written to the export. Unique within the catalog.\u003C/summary\u003E\r\n\tpublic string Id { get; set; }\r\n\r\n\t/// \u003Csummary\u003ELabel shown in the placement HUD / logs.\u003C/summary\u003E\r\n\tpublic string DisplayName { get; set; }\r\n\r\n\t/// \u003Csummary\u003EEngine model path, e.g. \u0022models/dev/box.vmdl\u0022. Used for the ghost preview and, when no\r\n\t/// \u003Csee cref=\u0022Prefab\u0022/\u003E is set, for the placed object (a GameObject with a single ModelRenderer).\u003C/summary\u003E\r\n\tpublic string ModelPath { get; set; }\r\n\r\n\t/// \u003Csummary\u003EOptional template GameObject cloned on place (a disabled prefab instance in the scene works\r\n\t/// well). Overrides \u003Csee cref=\u0022ModelPath\u0022/\u003E for the placed object; the ghost still uses ModelPath if set.\u003C/summary\u003E\r\n\tpublic GameObject Prefab { get; set; }\r\n\r\n\tpublic PlaceableEntry() { }\r\n\r\n\tpublic PlaceableEntry( string id, string displayName, string modelPath )\r\n\t{\r\n\t\tId = id;\r\n\t\tDisplayName = displayName;\r\n\t\tModelPath = modelPath;\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.placement","Path":"Placement/PlacedInstance.cs","FileName":"PlacedInstance.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337697,"Code":"using Sandbox;\r\n\r\nnamespace FieldGuide.Placement;\r\n\r\n/// \u003Csummary\u003E\r\n/// Tags a placed object with the catalog id it came from, so \u003Csee cref=\u0022PlacementExport\u0022/\u003E can collect\r\n/// every placed object in the scene and record which entry produced it. \u003Csee cref=\u0022GhostPlacer\u0022/\u003E adds\r\n/// this on place; you can also add it by hand (or in a prefab) to make an object show up in exports.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Placed Instance\u0022 )]\r\n[Category( \u0022Field Guide \u00B7 Placement\u0022 )]\r\n[Icon( \u0022place\u0022 )]\r\npublic sealed class PlacedInstance : Component\r\n{\r\n\t/// \u003Csummary\u003EThe \u003Csee cref=\u0022PlaceableEntry.Id\u0022/\u003E this object was placed from.\u003C/summary\u003E\r\n\t[Property] public string CatalogId { get; set; }\r\n}\r\n"},{"Ident":"fieldguide.placement","Path":"Code/Tweak/TweakPanel.razor.scss","FileName":"TweakPanel.razor.scss","PackageType":"library","CodeKind":"Game","AssetVersionId":337697,"Code":"// Tweak panel styling. 3 grouped font sizes (13 / 11 / 10), no letter-spacing (glyph-corruption\r\n// budget). Borders via border-width \u002B border-color only (never border-style, which aborts the whole\r\n// stylesheet). No blurred box-shadow on rounded elements (it renders unrounded); 0-blur rings only.\r\n// Root is pointer-events:none; only the card takes clicks, so the cursor stays usable over the panel\r\n// while the rest of the scene ignores it. Instructional text is bright, never dim gray.\r\n//\r\n// Slider line: minus stepper \u00B7 draggable fill track \u00B7 plus stepper. .tp-track position:relative \u002B\r\n// .tp-fill position:absolute so dragging fills the bar WITHOUT growing the whole row, and the fill is\r\n// pointer-events:none so the drag position stays measured against the track.\r\n//\r\n// The close control is a 42px square (owner ruling: 34px is too small to hit). There is no ESC badge\r\n// anywhere: the \u00D7 is the close affordance.\r\n\r\nTweakPanel {\r\n\tposition: absolute;\r\n\ttop: 0;\r\n\tleft: 0;\r\n\twidth: 100%;\r\n\theight: 100%;\r\n\tpointer-events: none;\r\n\r\n\t.tp-card {\r\n\t\tposition: absolute;\r\n\t\ttop: 80px;\r\n\t\tright: 24px;\r\n\t\twidth: 300px;\r\n\t\tflex-direction: column;\r\n\t\tpointer-events: all;\r\n\t\tbackground-color: rgba( 14, 16, 20, 0.94 );\r\n\t\tborder-width: 1px;\r\n\t\tborder-color: rgba( 90, 220, 255, 0.55 );\r\n\t\tborder-radius: 8px;\r\n\t\tpadding: 12px;\r\n\t\tfont-family: \u0022Roboto Mono\u0022, monospace;\r\n\t\tcolor: #eaf6ff;\r\n\t}\r\n\r\n\t.tp-hdr {\r\n\t\tflex-direction: row;\r\n\t\tjustify-content: space-between;\r\n\t\talign-items: center;\r\n\t\tmargin-bottom: 6px;\r\n\t}\r\n\t.tp-title {\r\n\t\tfont-size: 13px;\r\n\t\tcolor: #7fe6ff;\r\n\t}\r\n\t.tp-hr {\r\n\t\tflex-direction: row;\r\n\t\talign-items: center;\r\n\t}\r\n\t.tp-key {\r\n\t\tfont-size: 10px;\r\n\t\tcolor: #ffffff;\r\n\t\tfont-weight: 700;\r\n\t\tbackground-color: rgba( 90, 220, 255, 0.14 );\r\n\t\tborder-radius: 4px;\r\n\t\tpadding: 2px 6px;\r\n\t\tmargin-right: 6px;\r\n\t}\r\n\t.tp-x {\r\n\t\tfont-size: 13px;\r\n\t\tcolor: #ffffff;\r\n\t\twidth: 42px;\r\n\t\theight: 42px;\r\n\t\tjustify-content: center;\r\n\t\talign-items: center;\r\n\t\tborder-radius: 6px;\r\n\t\tborder-width: 1px;\r\n\t\tborder-color: rgba( 90, 220, 255, 0.45 );\r\n\t\tbackground-color: rgba( 255, 255, 255, 0.06 );\r\n\t\tpointer-events: all;\r\n\t\ttransition: all 0.1s ease;\r\n\t\t\u0026:hover {\r\n\t\t\tbackground-color: rgba( 90, 220, 255, 0.28 );\r\n\t\t\tborder-color: rgba( 90, 220, 255, 0.9 );\r\n\t\t}\r\n\t}\r\n\r\n\t.tp-empty {\r\n\t\tfont-size: 11px;\r\n\t\tcolor: #ffffff;\r\n\t\tpadding: 6px 0px;\r\n\t}\r\n\r\n\t.tp-tabs {\r\n\t\tflex-direction: row;\r\n\t\tflex-wrap: wrap;\r\n\t\tmargin-bottom: 8px;\r\n\t}\r\n\t.tp-tab {\r\n\t\tfont-size: 10px;\r\n\t\tcolor: #eaf6ff;\r\n\t\tbackground-color: rgba( 255, 255, 255, 0.06 );\r\n\t\tborder-radius: 4px;\r\n\t\tpadding: 4px 7px;\r\n\t\tmargin-right: 4px;\r\n\t\tmargin-bottom: 4px;\r\n\t\ttransition: all 0.1s ease;\r\n\t\t\u0026:hover { background-color: rgba( 255, 255, 255, 0.12 ); }\r\n\t\t\u0026.on {\r\n\t\t\tbackground-color: rgba( 90, 220, 255, 0.22 );\r\n\t\t\tcolor: #ffffff;\r\n\t\t\tborder-width: 1px;\r\n\t\t\tborder-color: rgba( 90, 220, 255, 0.7 );\r\n\t\t}\r\n\t}\r\n\r\n\t.tp-sub {\r\n\t\tflex-direction: column;\r\n\t\tmargin-bottom: 8px;\r\n\t}\r\n\t.tp-item { font-size: 11px; color: #ffffff; }\r\n\t.tp-note { font-size: 10px; color: #cfe0ea; }\r\n\r\n\t.tp-steprow {\r\n\t\tflex-direction: row;\r\n\t\talign-items: center;\r\n\t\tmargin-bottom: 8px;\r\n\t}\r\n\t.tp-sl { font-size: 10px; color: #cfe0ea; margin-right: 6px; }\r\n\t.tp-seg {\r\n\t\tfont-size: 10px;\r\n\t\tcolor: #eaf6ff;\r\n\t\tbackground-color: rgba( 255, 255, 255, 0.06 );\r\n\t\tborder-radius: 4px;\r\n\t\tpadding: 3px 7px;\r\n\t\tmargin-right: 4px;\r\n\t\ttransition: all 0.1s ease;\r\n\t\t\u0026:hover { background-color: rgba( 255, 255, 255, 0.12 ); }\r\n\t\t\u0026.on { background-color: rgba( 90, 220, 255, 0.22 ); color: #ffffff; }\r\n\t}\r\n\r\n\t.tp-row {\r\n\t\tflex-direction: column;\r\n\t\tmargin-bottom: 6px;\r\n\t\t\u0026.hdr {\r\n\t\t\tmargin-top: 6px;\r\n\t\t\tborder-top-width: 1px;\r\n\t\t\tborder-top-color: rgba( 255, 255, 255, 0.1 );\r\n\t\t\tpadding-top: 8px;\r\n\t\t}\r\n\t}\r\n\t.tp-rlab {\r\n\t\tflex-direction: row;\r\n\t\tjustify-content: space-between;\r\n\t\talign-items: center;\r\n\t\tmargin-bottom: 4px;\r\n\t}\r\n\t.tp-rl { font-size: 11px; color: #eaf6ff; }\r\n\t.tp-rv {\r\n\t\tfont-size: 11px;\r\n\t\tcolor: #ffffff;\r\n\t\tjustify-content: center;\r\n\t\ttext-align: center;\r\n\t}\r\n\r\n\t.tp-slider {\r\n\t\tflex-direction: row;\r\n\t\talign-items: center;\r\n\t}\r\n\t.tp-track {\r\n\t\tposition: relative;\r\n\t\tflex-grow: 1;\r\n\t\theight: 14px;\r\n\t\tmargin-left: 6px;\r\n\t\tmargin-right: 6px;\r\n\t\tborder-radius: 99px;\r\n\t\tbackground-color: rgba( 255, 255, 255, 0.10 );\r\n\t\tpointer-events: all;\r\n\t\t\u0026:hover { background-color: rgba( 255, 255, 255, 0.18 ); }\r\n\t\t.tp-fill {\r\n\t\t\tposition: absolute;\r\n\t\t\tleft: 0px;\r\n\t\t\theight: 100%;\r\n\t\t\tborder-radius: 99px;\r\n\t\t\tbackground-image: linear-gradient( 90deg, #3AC4DE, #7fe6ff );\r\n\t\t\tpointer-events: none;\r\n\t\t}\r\n\t}\r\n\t.tp-stp {\r\n\t\tfont-size: 13px;\r\n\t\tcolor: #eaf6ff;\r\n\t\tbackground-color: rgba( 255, 255, 255, 0.08 );\r\n\t\tborder-radius: 4px;\r\n\t\twidth: 24px;\r\n\t\theight: 22px;\r\n\t\tjustify-content: center;\r\n\t\talign-items: center;\r\n\t\ttransition: all 0.1s ease;\r\n\t\t\u0026:hover { background-color: rgba( 90, 220, 255, 0.28 ); color: #ffffff; }\r\n\t}\r\n\r\n\t.tp-btns {\r\n\t\tflex-direction: row;\r\n\t\tmargin-top: 8px;\r\n\t}\r\n\t.tp-btn {\r\n\t\tfont-size: 10px;\r\n\t\tcolor: #eaf6ff;\r\n\t\tbackground-color: rgba( 255, 255, 255, 0.08 );\r\n\t\tborder-radius: 4px;\r\n\t\tpadding: 7px 8px;\r\n\t\tmargin-right: 5px;\r\n\t\tjustify-content: center;\r\n\t\talign-items: center;\r\n\t\tflex-grow: 1;\r\n\t\ttransition: all 0.1s ease;\r\n\t\t\u0026:hover { background-color: rgba( 90, 220, 255, 0.24 ); color: #ffffff; }\r\n\t\t\u0026.wide { margin-right: 0px; }\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.placement","Path":"Code/Export/PlacementExport.cs","FileName":"PlacementExport.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337697,"Code":"using Sandbox;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Text;\r\n\r\nnamespace FieldGuide.Placement;\r\n\r\n/// \u003Csummary\u003E\r\n/// Bakes what you tuned in-game into text you can paste into your own project. Two populations are\r\n/// collected, because the kit serves two jobs and both end at \u0022get these numbers into my code\u0022:\r\n///\r\n///  1. TWEAK TARGETS (the hero case). Every \u003Csee cref=\u0022ITweakTarget\u0022/\u003E registered with the scene\u0027s\r\n///     \u003Csee cref=\u0022TweakSession\u0022/\u003E, exported as a LOCAL offset tagged with the frame it hangs off\r\n///     (\u0022citizen/hold_R\u0022, \u0022citizen/spine_2\u0022, ...). This is the accessory-fitting path: parent a custom\r\n///     model to a character, drag it into place, bake the offset.\r\n///  2. PLACED INSTANCES (scene authoring). Every object carrying a \u003Csee cref=\u0022PlacedInstance\u0022/\u003E, exported\r\n///     in WORLD space. This is the ghost-placer path: lay a scene out, bake the coordinates.\r\n///\r\n/// \u003Csee cref=\u0022WriteAll(Scene)\u0022/\u003E writes a JSON file and a C# file into FileSystem.Data (per-project,\r\n/// writable) and logs their full paths. The panel\u0027s Copy button does the single-target version:\r\n/// \u003Csee cref=\u0022BakeLine(ITweakTarget)\u0022/\u003E straight onto the clipboard.\r\n/// \u003C/summary\u003E\r\npublic static class PlacementExport\r\n{\r\n\tpublic const string JsonFileName = \u0022placement_export.json\u0022;\r\n\tpublic const string CSharpFileName = \u0022placement_export.cs\u0022;\r\n\r\n\t// ---- collection ----\r\n\r\n\t/// \u003Csummary\u003EBoth populations in one list: tweak-session offsets first (ordered by frame then id), then\r\n\t/// world-space placed instances. This is what the JSON and C# exports write.\u003C/summary\u003E\r\n\tpublic static List\u003CPlacedItem\u003E Collect( Scene scene )\r\n\t{\r\n\t\tvar list = new List\u003CPlacedItem\u003E();\r\n\t\tif ( scene is null ) return list;\r\n\r\n\t\tlist.AddRange( CollectOffsets( scene ) );\r\n\t\tlist.AddRange( CollectPlaced( scene ) );\r\n\t\treturn list;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe tweak session\u0027s targets as LOCAL offsets, each tagged with its frame. Empty when the\r\n\t/// scene has no \u003Csee cref=\u0022TweakSession\u0022/\u003E or the session holds no valid targets.\u003C/summary\u003E\r\n\tpublic static List\u003CPlacedItem\u003E CollectOffsets( Scene scene )\r\n\t{\r\n\t\tvar list = new List\u003CPlacedItem\u003E();\r\n\t\tif ( scene is null ) return list;\r\n\r\n\t\tforeach ( var session in scene.GetAllComponents\u003CTweakSession\u003E() )\r\n\t\t{\r\n\t\t\tif ( session is null || !session.IsValid() ) continue;\r\n\t\t\tforeach ( var target in session.Targets )\r\n\t\t\t{\r\n\t\t\t\tvar item = ToItem( target );\r\n\t\t\t\tif ( item is not null ) list.Add( item );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn list\r\n\t\t\t.OrderBy( i =\u003E i.Frame )\r\n\t\t\t.ThenBy( i =\u003E i.Id )\r\n\t\t\t.ToList();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EEvery object tagged with a \u003Csee cref=\u0022PlacedInstance\u0022/\u003E, in WORLD space, ordered by catalog\r\n\t/// id then x for stable output.\u003C/summary\u003E\r\n\tpublic static List\u003CPlacedItem\u003E CollectPlaced( Scene scene )\r\n\t{\r\n\t\tvar list = new List\u003CPlacedItem\u003E();\r\n\t\tif ( scene is null ) return list;\r\n\r\n\t\tforeach ( var inst in scene.GetAllComponents\u003CPlacedInstance\u003E() )\r\n\t\t{\r\n\t\t\tif ( inst is null || !inst.IsValid() ) continue;\r\n\t\t\tvar go = inst.GameObject;\r\n\t\t\tlist.Add( new PlacedItem(\r\n\t\t\t\tstring.IsNullOrEmpty( inst.CatalogId ) ? \u0022item\u0022 : inst.CatalogId,\r\n\t\t\t\tgo.WorldPosition,\r\n\t\t\t\tgo.WorldRotation.Angles(),\r\n\t\t\t\tgo.WorldScale.x,\r\n\t\t\t\tPlacedItem.WorldFrame ) );\r\n\t\t}\r\n\r\n\t\treturn list\r\n\t\t\t.OrderBy( i =\u003E i.Id )\r\n\t\t\t.ThenBy( i =\u003E i.Position.x )\r\n\t\t\t.ToList();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EOne tweak target\u0027s live LOCAL transform as a record: its bake id, its local position /\r\n\t/// rotation / uniform scale, and the frame it hangs off. Null for an invalid target.\u003C/summary\u003E\r\n\tpublic static PlacedItem ToItem( ITweakTarget target )\r\n\t{\r\n\t\tvar go = target?.Target;\r\n\t\tif ( go is null || !go.IsValid() ) return null;\r\n\r\n\t\treturn new PlacedItem(\r\n\t\t\ttarget.BakeSymbol,\r\n\t\t\tgo.LocalPosition,\r\n\t\t\tgo.LocalRotation.Angles(),\r\n\t\t\tgo.LocalScale.x,\r\n\t\t\ttarget.FrameName );\r\n\t}\r\n\r\n\t// ---- the paste-ready line (what Copy puts on the clipboard) ----\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The paste-ready C# for ONE tweak target. A target that implements\r\n\t/// \u003Csee cref=\u0022ITweakTarget.FormatBakeLine\u0022/\u003E shapes its own line (bake straight into your real data\r\n\t/// shape); otherwise the kit\u0027s default form is used. Returns an empty string for an invalid target.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static string BakeLine( ITweakTarget target )\r\n\t{\r\n\t\tvar item = ToItem( target );\r\n\t\tif ( item is null ) return \u0022\u0022;\r\n\r\n\t\tvar custom = target.FormatBakeLine( item );\r\n\t\treturn string.IsNullOrEmpty( custom ) ? DefaultBakeLine( item ) : custom;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The kit\u0027s default bake line: a comment naming the id and the frame the offset is measured from,\r\n\t/// then a \u003Csee cref=\u0022PlacedOffset\u0022/\u003E constructor with the live numbers. Two lines, because the comment\r\n\t/// is what makes the numbers mean something once they are sitting in a file three weeks later.\r\n\t/// \u003Ccode\u003E\r\n\t/// // hat: offset from citizen/head\r\n\t/// new PlacedOffset( \u0022hat\u0022, new Vector3( 0.5f, 0f, 3.25f ), new Angles( 0f, 90f, 0f ), 1f ),\r\n\t/// \u003C/code\u003E\r\n\t/// \u003C/summary\u003E\r\n\tpublic static string DefaultBakeLine( PlacedItem it )\r\n\t{\r\n\t\tif ( it is null ) return \u0022\u0022;\r\n\t\tstring what = it.IsLocalOffset ? $\u0022offset from {it.Frame}\u0022 : \u0022world placement\u0022;\r\n\t\treturn $\u0022// {it.Id}: {what}\\n{OffsetCtor( it )},\u0022;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EJust the constructor expression, no comment and no trailing comma. The building block the\r\n\t/// default line and the C# file share.\u003C/summary\u003E\r\n\tpublic static string OffsetCtor( PlacedItem it )\r\n\t\t=\u003E $\u0022new PlacedOffset( \\\u0022{BakeFormat.Escape( it.Id )}\\\u0022, \u0022\r\n\t\t \u002B $\u0022new Vector3( {BakeFormat.F( it.Position.x )}f, {BakeFormat.F( it.Position.y )}f, {BakeFormat.F( it.Position.z )}f ), \u0022\r\n\t\t \u002B $\u0022new Angles( {BakeFormat.F( it.Rotation.pitch )}f, {BakeFormat.F( it.Rotation.yaw )}f, {BakeFormat.F( it.Rotation.roll )}f ), \u0022\r\n\t\t \u002B $\u0022{BakeFormat.F( it.Scale )}f )\u0022;\r\n\r\n\t// ---- JSON ----\r\n\r\n\t/// \u003Csummary\u003EAn indented JSON array of { id, frame, position {x,y,z}, rotation {pitch,yaw,roll}, scale }.\r\n\t/// \u0022frame\u0022 is \u0022world\u0022 for a placed instance and the target\u0027s frame name for a tuned offset.\u003C/summary\u003E\r\n\tpublic static string ToJson( IEnumerable\u003CPlacedItem\u003E items )\r\n\t{\r\n\t\tvar sb = new StringBuilder();\r\n\t\tsb.Append( \u0022[\\n\u0022 );\r\n\t\tvar arr = items.ToList();\r\n\t\tfor ( int i = 0; i \u003C arr.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar it = arr[i];\r\n\t\t\tsb.Append( \u0022  {\\n\u0022 );\r\n\t\t\tsb.Append( $\u0022    \\\u0022id\\\u0022: \\\u0022{BakeFormat.Escape( it.Id )}\\\u0022,\\n\u0022 );\r\n\t\t\tsb.Append( $\u0022    \\\u0022frame\\\u0022: \\\u0022{BakeFormat.Escape( it.Frame )}\\\u0022,\\n\u0022 );\r\n\t\t\tsb.Append( $\u0022    \\\u0022position\\\u0022: {{ \\\u0022x\\\u0022: {BakeFormat.F( it.Position.x )}, \\\u0022y\\\u0022: {BakeFormat.F( it.Position.y )}, \\\u0022z\\\u0022: {BakeFormat.F( it.Position.z )} }},\\n\u0022 );\r\n\t\t\tsb.Append( $\u0022    \\\u0022rotation\\\u0022: {{ \\\u0022pitch\\\u0022: {BakeFormat.F( it.Rotation.pitch )}, \\\u0022yaw\\\u0022: {BakeFormat.F( it.Rotation.yaw )}, \\\u0022roll\\\u0022: {BakeFormat.F( it.Rotation.roll )} }},\\n\u0022 );\r\n\t\t\tsb.Append( $\u0022    \\\u0022scale\\\u0022: {BakeFormat.F( it.Scale )}\\n\u0022 );\r\n\t\t\tsb.Append( i \u003C arr.Count - 1 ? \u0022  },\\n\u0022 : \u0022  }\\n\u0022 );\r\n\t\t}\r\n\t\tsb.Append( \u0022]\\n\u0022 );\r\n\t\treturn sb.ToString();\r\n\t}\r\n\r\n\t// ---- C# snippet ----\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// A paste-ready C# file with up to two blocks, each written only when it has entries:\r\n\t/// a \u003Cc\u003EPlacedOffset[]\u003C/c\u003E of character-relative offsets (one commented line per accessory, naming the\r\n\t/// frame it hangs off), and a \u003Cc\u003EPlacedItem[]\u003C/c\u003E of world-space placements. Both record types ship\r\n\t/// with the kit, so the paste compiles as-is against \u003Cc\u003EFieldGuide.Placement\u003C/c\u003E.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static string ToCSharp( IEnumerable\u003CPlacedItem\u003E items )\r\n\t{\r\n\t\tvar arr = items.ToList();\r\n\t\tvar offsets = arr.Where( i =\u003E i.IsLocalOffset ).ToList();\r\n\t\tvar placed = arr.Where( i =\u003E !i.IsLocalOffset ).ToList();\r\n\r\n\t\tvar sb = new StringBuilder();\r\n\t\tsb.Append( \u0022// Generated by FieldGuide.Placement. Paste into your project.\\n\u0022 );\r\n\r\n\t\tif ( offsets.Count \u003E 0 )\r\n\t\t{\r\n\t\t\tsb.Append( \u0022\\n// Character-relative offsets. Each is LOCAL to the frame named above it: parent your\\n\u0022 );\r\n\t\t\tsb.Append( \u0022// object to that mount, then call offset.ApplyTo( go ).\\n\u0022 );\r\n\t\t\tsb.Append( \u0022static readonly PlacedOffset[] Offsets =\\n{\\n\u0022 );\r\n\t\t\tforeach ( var it in offsets )\r\n\t\t\t{\r\n\t\t\t\tsb.Append( $\u0022\\t// {it.Id}: offset from {it.Frame}\\n\u0022 );\r\n\t\t\t\tsb.Append( $\u0022\\t{OffsetCtor( it )},\\n\u0022 );\r\n\t\t\t}\r\n\t\t\tsb.Append( \u0022};\\n\u0022 );\r\n\t\t}\r\n\r\n\t\tif ( placed.Count \u003E 0 )\r\n\t\t{\r\n\t\t\tsb.Append( \u0022\\n// World-space placements from the ghost placer.\\n\u0022 );\r\n\t\t\tsb.Append( \u0022// PlacedItem( string Id, Vector3 Position, Angles Rotation, float Scale, string Frame )\\n\u0022 );\r\n\t\t\tsb.Append( \u0022static readonly PlacedItem[] Placed =\\n{\\n\u0022 );\r\n\t\t\tforeach ( var it in placed )\r\n\t\t\t{\r\n\t\t\t\tsb.Append( $\u0022\\tnew PlacedItem( \\\u0022{BakeFormat.Escape( it.Id )}\\\u0022, \u0022 );\r\n\t\t\t\tsb.Append( $\u0022new Vector3( {BakeFormat.F( it.Position.x )}f, {BakeFormat.F( it.Position.y )}f, {BakeFormat.F( it.Position.z )}f ), \u0022 );\r\n\t\t\t\tsb.Append( $\u0022new Angles( {BakeFormat.F( it.Rotation.pitch )}f, {BakeFormat.F( it.Rotation.yaw )}f, {BakeFormat.F( it.Rotation.roll )}f ), \u0022 );\r\n\t\t\t\tsb.Append( $\u0022{BakeFormat.F( it.Scale )}f, \\\u0022{BakeFormat.Escape( it.Frame )}\\\u0022 ),\\n\u0022 );\r\n\t\t\t}\r\n\t\t\tsb.Append( \u0022};\\n\u0022 );\r\n\t\t}\r\n\r\n\t\tif ( offsets.Count == 0 \u0026\u0026 placed.Count == 0 )\r\n\t\t\tsb.Append( \u0022\\n// Nothing to export: no TweakSession targets and no PlacedInstance objects in the scene.\\n\u0022 );\r\n\r\n\t\treturn sb.ToString();\r\n\t}\r\n\r\n\t// ---- write both to FileSystem.Data ----\r\n\r\n\t/// \u003Csummary\u003ECollect both populations and write the JSON and C# exports to FileSystem.Data, logging the\r\n\t/// paths and the per-population counts. Returns the total number of records written.\u003C/summary\u003E\r\n\tpublic static int WriteAll( Scene scene )\r\n\t{\r\n\t\tvar items = Collect( scene );\r\n\t\tint offsets = items.Count( i =\u003E i.IsLocalOffset );\r\n\t\tint placed = items.Count - offsets;\r\n\r\n\t\tFileSystem.Data.WriteAllText( JsonFileName, ToJson( items ) );\r\n\t\tFileSystem.Data.WriteAllText( CSharpFileName, ToCSharp( items ) );\r\n\r\n\t\tLog.Info( $\u0022[placement] exported {offsets} tuned offset(s) \u002B {placed} world placement(s):\u0022 );\r\n\t\tLog.Info( $\u0022[placement]   JSON to {FileSystem.Data.GetFullPath( JsonFileName )}\u0022 );\r\n\t\tLog.Info( $\u0022[placement]   C# to {FileSystem.Data.GetFullPath( CSharpFileName )}\u0022 );\r\n\t\treturn items.Count;\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.placement","Path":"Placement/GhostPlacer.cs","FileName":"GhostPlacer.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337697,"Code":"using Sandbox;\r\nusing System;\r\nusing System.Linq;\r\n\r\nnamespace FieldGuide.Placement;\r\n\r\n/// \u003Csummary\u003E\r\n/// Aim-follow ghost placement, pattern-rebuilt from World Builder\u0027s prop placer, minus all networking:\r\n/// this is a single-player authoring driver. Toggle placement mode with B (or the \u0060placement_place\u0060\r\n/// convar); a tinted, colliderless GHOST of the current catalog entry follows the aim ray (green =\r\n/// valid spot, red = invalid, per \u003Csee cref=\u0022PlacementCatalog.ValidityCheck\u0022/\u003E). Bracket keys cycle\r\n/// the catalog, R rotates the ghost, left mouse places, G deletes the placed object under the cursor.\r\n///\r\n/// Placed objects get a \u003Csee cref=\u0022PlacedInstance\u0022/\u003E tag so \u003Csee cref=\u0022PlacementExport\u0022/\u003E can find them.\r\n/// Works in engine units throughout (no meter scaling), so it composes with any camera setup.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Ghost Placer\u0022 )]\r\n[Category( \u0022Field Guide \u00B7 Placement\u0022 )]\r\n[Icon( \u0022add_location\u0022 )]\r\npublic sealed class GhostPlacer : Component\r\n{\r\n\tpublic static GhostPlacer Instance { get; private set; }\r\n\r\n\t// \u2500\u2500 HUD readback (static so a HUD razor needs no component handle) \u2500\u2500\r\n\tpublic static bool PlaceModeActive { get; private set; }\r\n\tpublic static string CurrentEntryName { get; private set; }\r\n\tpublic static int PlacedCount { get; private set; }\r\n\r\n\t// \u2500\u2500 toggle state (B raw key \u002B \u0060placement_place\u0060 convar fallback) \u2500\u2500\r\n\tstatic bool _placeConvar;\r\n\t[ConVar( \u0022placement_place\u0022, Help = \u0022Arm or disarm ghost placement mode (same as the B key)\u0022 )]\r\n\tpublic static bool PlaceModeConVar { get =\u003E _placeConvar; set =\u003E _placeConvar = value; }\r\n\r\n\t/// \u003Csummary\u003ERotation step (degrees) applied by the R key.\u003C/summary\u003E\r\n\t[Property] public float RotateStepDeg { get; set; } = 45f;\r\n\r\n\t/// \u003Csummary\u003EHow far the placed/ghost object sinks along -Z from the aim hit (engine units).\u003C/summary\u003E\r\n\t[Property] public float SinkDepth { get; set; } = 0f;\r\n\r\n\t/// \u003Csummary\u003EMax aim-ray length in engine units.\u003C/summary\u003E\r\n\t[Property] public float AimReach { get; set; } = 500_000f;\r\n\r\n\tbool _placeMode;\r\n\tint _catalogIndex;\r\n\tfloat _ghostYaw;\r\n\tGameObject _ghost;\r\n\tstring _ghostModel;\r\n\r\n\tPlacementCatalog Catalog =\u003E PlacementCatalog.Instance;\r\n\r\n\tPlaceableEntry CurrentEntry\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar cat = Catalog;\r\n\t\t\tif ( cat is null || cat.Entries.Count == 0 ) return null;\r\n\t\t\tint n = cat.Entries.Count;\r\n\t\t\treturn cat.Entries[((_catalogIndex % n) \u002B n) % n];\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnEnabled() =\u003E Instance = this;\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tif ( Instance == this ) Instance = null;\r\n\t\tPlaceModeActive = false;\r\n\t\tDestroyGhost();\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tHandleInput();\r\n\t\tUpdateGhost();\r\n\r\n\t\tPlaceModeActive = _placeMode;\r\n\t\tCurrentEntryName = CurrentEntry?.DisplayName;\r\n\t\tPlacedCount = CountPlaced();\r\n\t}\r\n\r\n\t// \u2500\u2500 input \u2500\u2500\r\n\r\n\tvoid HandleInput()\r\n\t{\r\n\t\t// B (raw key) or the \u0060placement_place\u0060 convar toggles placement mode.\r\n\t\tif ( Input.Keyboard.Pressed( \u0022B\u0022 ) )\r\n\t\t\tSetPlaceMode( !_placeMode );\r\n\t\telse if ( _placeConvar != _placeMode )\r\n\t\t\tSetPlaceMode( _placeConvar );\r\n\r\n\t\tif ( !_placeMode ) return;\r\n\r\n\t\t// cycle the catalog (bracket keys; the mouse wheel is the camera zoom, so it is not used here)\r\n\t\tif ( Input.Keyboard.Pressed( \u0022]\u0022 ) ) { _catalogIndex\u002B\u002B; RebuildGhostModel(); }\r\n\t\tif ( Input.Keyboard.Pressed( \u0022[\u0022 ) ) { _catalogIndex--; RebuildGhostModel(); }\r\n\r\n\t\t// rotate (R)\r\n\t\tif ( Input.Keyboard.Pressed( \u0022R\u0022 ) ) _ghostYaw = (_ghostYaw \u002B RotateStepDeg) % 360f;\r\n\r\n\t\t// place (left mouse)\r\n\t\tif ( Input.Pressed( \u0022attack1\u0022 ) )\r\n\t\t\tTryPlaceAtAim();\r\n\r\n\t\t// delete the placed object under the cursor (G)\r\n\t\tif ( Input.Keyboard.Pressed( \u0022G\u0022 ) )\r\n\t\t\tTryDeleteAtAim();\r\n\t}\r\n\r\n\tvoid SetPlaceMode( bool on )\r\n\t{\r\n\t\t_placeMode = on;\r\n\t\t_placeConvar = on;\r\n\t\tif ( on )\r\n\t\t{\r\n\t\t\tRebuildGhostModel();\r\n\t\t\tMouse.Visibility = MouseVisibility.Visible;\r\n\t\t\tLog.Info( $\u0022[placement] mode ON, armed with {CurrentEntry?.DisplayName ?? \u0022(empty catalog)\u0022}\u0022 );\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tDestroyGhost();\r\n\t\t\tLog.Info( \u0022[placement] mode OFF\u0022 );\r\n\t\t}\r\n\t}\r\n\r\n\t// \u2500\u2500 aim \u2500\u2500\r\n\r\n\tbool AimHit( out Vector3 worldPos )\r\n\t{\r\n\t\tworldPos = default;\r\n\t\tvar cam = Scene.Camera;\r\n\t\tif ( !cam.IsValid() ) return false;\r\n\t\tvar ray = cam.ScreenPixelToRay( Mouse.Position );\r\n\t\tvar trace = Scene.Trace.Ray( ray, AimReach );\r\n\t\tif ( _ghost.IsValid() ) trace = trace.IgnoreGameObjectHierarchy( _ghost );   // never hit the ghost itself\r\n\t\tvar tr = trace.Run();\r\n\t\tif ( !tr.Hit || tr.StartedSolid ) return false;\r\n\t\tworldPos = tr.HitPosition;\r\n\t\treturn true;\r\n\t}\r\n\r\n\tvoid TryPlaceAtAim()\r\n\t{\r\n\t\tvar entry = CurrentEntry;\r\n\t\tif ( entry is null || !AimHit( out var hit ) ) return;\r\n\r\n\t\tvar pos = hit.WithZ( hit.z - SinkDepth );\r\n\t\tvar rot = Rotation.FromYaw( _ghostYaw );\r\n\r\n\t\tif ( !(Catalog?.IsValidSpot( pos, rot ) ?? true) )\r\n\t\t{\r\n\t\t\tLog.Info( $\u0022[placement] refused: invalid spot for {entry.DisplayName}\u0022 );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar go = Spawn( entry, pos, rot );\r\n\t\tif ( go is null )\r\n\t\t{\r\n\t\t\tLog.Info( $\u0022[placement] could not spawn {entry.DisplayName} (no prefab or model)\u0022 );\r\n\t\t\treturn;\r\n\t\t}\r\n\t\tLog.Info( $\u0022[placement] placed {entry.Id} at {pos} yaw={_ghostYaw:0} count={CountPlaced()}\u0022 );\r\n\t}\r\n\r\n\tGameObject Spawn( PlaceableEntry entry, Vector3 pos, Rotation rot )\r\n\t{\r\n\t\tGameObject go = null;\r\n\t\tif ( entry.Prefab.IsValid() )\r\n\t\t{\r\n\t\t\tgo = entry.Prefab.Clone( pos, rot );\r\n\t\t}\r\n\t\telse if ( !string.IsNullOrEmpty( entry.ModelPath ) )\r\n\t\t{\r\n\t\t\tgo = Scene.CreateObject();\r\n\t\t\tgo.WorldPosition = pos;\r\n\t\t\tgo.WorldRotation = rot;\r\n\t\t\tvar r = go.Components.Create\u003CModelRenderer\u003E();\r\n\t\t\tvar m = Model.Load( entry.ModelPath );\r\n\t\t\tif ( m is not null \u0026\u0026 !m.IsError ) r.Model = m;\r\n\t\t}\r\n\t\tif ( go is null ) return null;\r\n\r\n\t\tgo.Name = $\u0022placed_{entry.Id}\u0022;\r\n\t\tgo.Components.GetOrCreate\u003CPlacedInstance\u003E().CatalogId = entry.Id;\r\n\t\treturn go;\r\n\t}\r\n\r\n\tvoid TryDeleteAtAim()\r\n\t{\r\n\t\tvar cam = Scene.Camera;\r\n\t\tif ( !cam.IsValid() ) return;\r\n\t\tvar ray = cam.ScreenPixelToRay( Mouse.Position );\r\n\t\tvar trace = Scene.Trace.Ray( ray, AimReach );\r\n\t\tif ( _ghost.IsValid() ) trace = trace.IgnoreGameObjectHierarchy( _ghost );\r\n\t\tvar tr = trace.Run();\r\n\t\tif ( !tr.Hit ) return;\r\n\r\n\t\tvar placed = tr.GameObject?.Components.GetInAncestorsOrSelf\u003CPlacedInstance\u003E();\r\n\t\tif ( placed is null || !placed.IsValid() ) return;\r\n\t\tLog.Info( $\u0022[placement] deleted {placed.CatalogId}\u0022 );\r\n\t\tplaced.GameObject.Destroy();\r\n\t}\r\n\r\n\t// \u2500\u2500 ghost preview (colliderless, tinted) \u2500\u2500\r\n\r\n\tvoid RebuildGhostModel() =\u003E _ghostModel = null;\r\n\r\n\tvoid UpdateGhost()\r\n\t{\r\n\t\tif ( !_placeMode ) { DestroyGhost(); return; }\r\n\r\n\t\tvar entry = CurrentEntry;\r\n\t\tif ( entry is null || !AimHit( out var hit ) )\r\n\t\t{\r\n\t\t\tif ( _ghost.IsValid() ) _ghost.Enabled = false;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tEnsureGhost( entry );\r\n\t\tif ( !_ghost.IsValid() ) return;\r\n\t\t_ghost.Enabled = true;\r\n\r\n\t\tvar pos = hit.WithZ( hit.z - SinkDepth );\r\n\t\tvar rot = Rotation.FromYaw( _ghostYaw );\r\n\t\t_ghost.WorldPosition = pos;\r\n\t\t_ghost.WorldRotation = rot;\r\n\r\n\t\tbool valid = Catalog?.IsValidSpot( pos, rot ) ?? true;\r\n\t\tvar r = _ghost.Components.Get\u003CModelRenderer\u003E();\r\n\t\tif ( r is not null )\r\n\t\t\tr.Tint = valid ? new Color( 0.5f, 1f, 0.55f, 0.55f ) : new Color( 1f, 0.45f, 0.4f, 0.55f );\r\n\t}\r\n\r\n\tvoid EnsureGhost( PlaceableEntry entry )\r\n\t{\r\n\t\tstring modelPath = entry.ModelPath;\r\n\t\tif ( string.IsNullOrEmpty( modelPath ) ) { DestroyGhost(); return; }   // prefab-only entries: no light ghost\r\n\r\n\t\tif ( !_ghost.IsValid() )\r\n\t\t{\r\n\t\t\t_ghost = Scene.CreateObject();\r\n\t\t\t_ghost.Name = \u0022placement_ghost\u0022;\r\n\t\t\t_ghost.Components.Create\u003CModelRenderer\u003E();\r\n\t\t\t_ghostModel = null;\r\n\t\t}\r\n\t\tif ( _ghostModel != modelPath )\r\n\t\t{\r\n\t\t\tvar r = _ghost.Components.Get\u003CModelRenderer\u003E();\r\n\t\t\tvar m = Model.Load( modelPath );\r\n\t\t\tif ( m is not null \u0026\u0026 !m.IsError ) { r.Model = m; _ghostModel = modelPath; }\r\n\t\t}\r\n\t}\r\n\r\n\tvoid DestroyGhost()\r\n\t{\r\n\t\tif ( _ghost.IsValid() ) { _ghost.Destroy(); _ghost = null; }\r\n\t\t_ghostModel = null;\r\n\t}\r\n\r\n\tint CountPlaced() =\u003E Scene?.GetAllComponents\u003CPlacedInstance\u003E().Count() ?? 0;\r\n}\r\n"},{"Ident":"fieldguide.placement","Path":"Tweak/TweakRanges.cs","FileName":"TweakRanges.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337697,"Code":"namespace FieldGuide.Placement;\r\n\r\n/// \u003Csummary\u003E\r\n/// Slider bounds and step sizes for one tweak target\u0027s rows, in ENGINE UNITS and degrees.\r\n///\r\n/// Why this is per-target and not a constant: fitting a hat to a head and laying out a building are the\r\n/// same three sliders over wildly different ranges. A position row spanning \u002B/-1024 units at step 1 (the\r\n/// kit\u0027s original fixed range) cannot seat an accessory at all: one pixel of drag is several units, and\r\n/// the value you want lives in the first half-percent of the track. The accessory default below spans\r\n/// \u002B/-48 units at step 0.25, so the xfine step (\u00F710) reaches 0.025 units and a full drag still covers a\r\n/// character-sized volume.\r\n/// \u003C/summary\u003E\r\npublic sealed record TweakRanges(\r\n\tfloat PositionRange = 48f,\r\n\tfloat PositionStep = 0.25f,\r\n\tfloat RotationStep = 1f,\r\n\tfloat ScaleMin = 0.05f,\r\n\tfloat ScaleMax = 4f,\r\n\tfloat ScaleStep = 0.05f )\r\n{\r\n\t/// \u003Csummary\u003EThe default: character-accessory scale. \u002B/-48 units at 0.25 (xfine reaches 0.025), rotation\r\n\t/// at 1 degree, scale 0.05 to 4. This is what an unconfigured target gets.\u003C/summary\u003E\r\n\tpublic static readonly TweakRanges Accessory = new();\r\n\r\n\t/// \u003Csummary\u003EScene-authoring scale, for tweaking a placed prop rather than a worn accessory:\r\n\t/// \u002B/-1024 units at step 1, scale up to 8.\u003C/summary\u003E\r\n\tpublic static readonly TweakRanges Scene = new( PositionRange: 1024f, PositionStep: 1f, ScaleMax: 8f );\r\n\r\n\t/// \u003Csummary\u003ERoom-scale, between the two: \u002B/-256 units at 0.5.\u003C/summary\u003E\r\n\tpublic static readonly TweakRanges Prop = new( PositionRange: 256f, PositionStep: 0.5f );\r\n}\r\n"},{"Ident":"fieldguide.placement","Path":"Tweak/TweakPanel.razor","FileName":"TweakPanel.razor","PackageType":"library","CodeKind":"Game","AssetVersionId":337697,"Code":"@using Sandbox\r\n@using Sandbox.UI\r\n@using System\r\n@using System.Collections.Generic\r\n@namespace FieldGuide.Placement\r\n@inherits PanelComponent\r\n@attribute [StyleSheet]\r\n\r\n@*\r\n\tA live transform tuner for objects hanging off something else: an accessory on a character mount, a\r\n\tpart on a vehicle, a prop in a scene. One tab per ITweakTarget supplied by the scene\u0027s TweakSession;\r\n\tthe active tab edits that target GameObject\u0027s LocalPosition, LocalRotation (as pitch / yaw / roll) and\r\n\tuniform LocalScale, so what you drag is the OFFSET, which is the number your game code wants.\r\n\r\n\tEach row is a draggable slider (click-jump \u002B drag-scrub, the engine SliderControl mechanic) with a\r\n\tsmall \u002B/- stepper beside it for precision a drag can\u0027t hit; the three-state step toggle divides or\r\n\tmultiplies that stepper by ten. Row ranges come from the target\u0027s TweakRanges, so an accessory gets\r\n\tcharacter-scale sliders and a scene prop gets scene-scale ones.\r\n\r\n\tCopy puts the active target\u0027s paste-ready bake line on the clipboard (game-side Clipboard.SetText).\r\n\tExport writes every target and every placed object into FileSystem.Data as JSON plus a C# snippet.\r\n\tReset restores the transform captured when the target was registered, never zero.\r\n\r\n\tRows render in MAIN markup via @foreach (not a RenderFragment) per the fragment-undermeasure gotcha,\r\n\tand add shapes only (track/fill), keeping the text-run count low. Toggle with P (raw key, letter,\r\n\tnever an F key), the 42px x in the header, or the \u0060placement_panel\u0060 console convar. Starts closed\r\n\tunless OpenOnStart is set; see the boot block in OnUpdate.\r\n*@\r\n\r\n\u003Croot\u003E\r\n@if ( PanelOpen )\r\n{\r\n\t\u003Cdiv class=\u0022tp-card\u0022\u003E\r\n\t\t\u003Cdiv class=\u0022tp-hdr\u0022\u003E\r\n\t\t\t\u003Cspan class=\u0022tp-title\u0022\u003ETRANSFORM TWEAK\u003C/span\u003E\r\n\t\t\t\u003Cdiv class=\u0022tp-hr\u0022\u003E\r\n\t\t\t\t\u003Cspan class=\u0022tp-key\u0022\u003EP\u003C/span\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022tp-x\u0022 onclick=@ClosePanel\u003E\u00D7\u003C/div\u003E\r\n\t\t\t\u003C/div\u003E\r\n\t\t\u003C/div\u003E\r\n\r\n\t\t@if ( TargetList.Count == 0 )\r\n\t\t{\r\n\t\t\t\u003Cdiv class=\u0022tp-empty\u0022\u003ENo targets. Add GameObjects to a TweakSession (Objects list) or call TweakSession.Add(...).\u003C/div\u003E\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\t@* ---- one tab per target ---- *@\r\n\t\t\t\u003Cdiv class=\u0022tp-tabs\u0022\u003E\r\n\t\t\t\t@for ( int i = 0; i \u003C TargetList.Count; i\u002B\u002B )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar idx = i;\r\n\t\t\t\t\tstring tab = TargetList[idx].DisplayName;\r\n\t\t\t\t\t\u003Cdiv class=\u0022tp-tab @(idx == ActiveIndex ? \u0022on\u0022 : \u0022\u0022)\u0022 onclick=@(() =\u003E SelectTab( idx ))\u003E@tab\u003C/div\u003E\r\n\t\t\t\t}\r\n\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t\u003Cdiv class=\u0022tp-sub\u0022\u003E\r\n\t\t\t\t\u003Cspan class=\u0022tp-item\u0022\u003E@ActiveName\u003C/span\u003E\r\n\t\t\t\t\u003Cspan class=\u0022tp-note\u0022\u003E@FrameNote\u003C/span\u003E\r\n\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t@* ---- step-size toggle ---- *@\r\n\t\t\t\u003Cdiv class=\u0022tp-steprow\u0022\u003E\r\n\t\t\t\t\u003Cspan class=\u0022tp-sl\u0022\u003EStep\u003C/span\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022tp-seg @(_step == StepSize.XFine ? \u0022on\u0022 : \u0022\u0022)\u0022 onclick=@(() =\u003E _step = StepSize.XFine)\u003Exfine \u00F710\u003C/div\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022tp-seg @(_step == StepSize.Fine ? \u0022on\u0022 : \u0022\u0022)\u0022 onclick=@(() =\u003E _step = StepSize.Fine)\u003Efine\u003C/div\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022tp-seg @(_step == StepSize.Coarse ? \u0022on\u0022 : \u0022\u0022)\u0022 onclick=@(() =\u003E _step = StepSize.Coarse)\u003Ecoarse \u00D710\u003C/div\u003E\r\n\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t@* ---- editable rows (draggable slider \u002B stepper) ---- *@\r\n\t\t\t@foreach ( var r in Rows )\r\n\t\t\t{\r\n\t\t\t\tvar row = r;\r\n\t\t\t\tfloat cur = Get( ActiveTarget, row.field );\r\n\t\t\t\tstring val = cur.ToString( row.fmt );\r\n\t\t\t\tstring lab = row.label;   // plain local before interpolating: an inline field read can render blank\r\n\t\t\t\tint fillPct = (int)( Frac( cur, row ) * 100f );\r\n\t\t\t\t\u003Cdiv class=\u0022tp-row @(row.header ? \u0022hdr\u0022 : \u0022\u0022)\u0022\u003E\r\n\t\t\t\t\t\u003Cdiv class=\u0022tp-rlab\u0022\u003E\r\n\t\t\t\t\t\t\u003Cspan class=\u0022tp-rl\u0022\u003E@lab\u003C/span\u003E\r\n\t\t\t\t\t\t\u003Cspan class=\u0022tp-rv\u0022\u003E@val\u003C/span\u003E\r\n\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\u003Cdiv class=\u0022tp-slider\u0022\u003E\r\n\t\t\t\t\t\t\u003Cspan class=\u0022tp-stp\u0022 onclick=@(() =\u003E Nudge( row.field, -row.step * StepMult ))\u003E\u2212\u003C/span\u003E\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022tp-track\u0022\r\n\t\t\t\t\t\t\tonmousedown=@(e =\u003E TrackPointer( e, row, true ))\r\n\t\t\t\t\t\t\tonmousemove=@(e =\u003E TrackPointer( e, row, false ))\u003E\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022tp-fill\u0022 style=\u0022width: @(fillPct)%;\u0022\u003E\u003C/div\u003E\r\n\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\u003Cspan class=\u0022tp-stp\u0022 onclick=@(() =\u003E Nudge( row.field, row.step * StepMult ))\u003E\u002B\u003C/span\u003E\r\n\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\u003C/div\u003E\r\n\t\t\t}\r\n\r\n\t\t\t@* ---- actions ---- *@\r\n\t\t\t\u003Cdiv class=\u0022tp-btns\u0022\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022tp-btn\u0022 onclick=@ResetActive\u003EReset\u003C/div\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022tp-btn\u0022 onclick=@CopyActive\u003E@_copyLabel\u003C/div\u003E\r\n\t\t\t\u003C/div\u003E\r\n\t\t\t\u003Cdiv class=\u0022tp-btns\u0022\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022tp-btn wide\u0022 onclick=@ExportNow\u003EExport all to Data\u003C/div\u003E\r\n\t\t\t\u003C/div\u003E\r\n\t\t}\r\n\t\u003C/div\u003E\r\n}\r\n\u003C/root\u003E\r\n\r\n@code\r\n{\r\n\t// ---- toggle state (P raw key \u002B \u0060placement_panel\u0060 convar fallback) ----\r\n\tstatic bool _open;\r\n\r\n\t/// \u003Csummary\u003EConsole fallback: \u0060placement_panel 1\u0060 / \u0060placement_panel 0\u0060 toggles the panel (P also toggles).\u003C/summary\u003E\r\n\t[ConVar( \u0022placement_panel\u0022, Help = \u0022Open or close the transform tweak panel (same as the P key)\u0022 )]\r\n\tpublic static bool PanelOpen { get =\u003E _open; set =\u003E _open = value; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Whether this panel starts open. Off by default: a tuning panel that appears unbidden over a\r\n\t/// consumer\u0027s game is a bug, not a feature. Turn it on for a scene whose whole point is the panel, the\r\n\t/// way the kit\u0027s own demo does.\r\n\t///\r\n\t/// This is what decides the panel\u0027s boot state, and it is the ONLY thing that decides it. See the boot\r\n\t/// block in OnUpdate for why that matters.\r\n\t/// \u003C/summary\u003E\r\n\t[Property] public bool OpenOnStart { get; set; }\r\n\r\n\tint _activeIndex;\r\n\tstring _copyLabel = \u0022Copy\u0022;\r\n\tbool _wasOpen;\r\n\tbool _booted;\r\n\r\n\t/// \u003Csummary\u003EThree-state stepper size (owner ruling, ported from the World Builder mount tuner): xfine for\r\n\t/// final seating, fine for normal work, coarse for getting into the neighbourhood.\u003C/summary\u003E\r\n\tenum StepSize { XFine, Fine, Coarse }\r\n\tStepSize _step = StepSize.Fine;\r\n\r\n\tIReadOnlyList\u003CITweakTarget\u003E TargetList =\u003E TweakSession.Instance?.Targets ?? System.Array.Empty\u003CITweakTarget\u003E();\r\n\r\n\tint ActiveIndex\r\n\t{\r\n\t\tget =\u003E TargetList.Count == 0 ? 0 : Math.Clamp( _activeIndex, 0, TargetList.Count - 1 );\r\n\t\tset =\u003E _activeIndex = value;\r\n\t}\r\n\r\n\t// Named ActiveTarget (not Active): PanelComponent/Component already exposes an inherited\r\n\t// bool Active, and a razor-generated member named Active would hide it (CS0108).\r\n\tITweakTarget ActiveTarget =\u003E TargetList.Count == 0 ? null : TargetList[ActiveIndex];\r\n\r\n\t/// \u003Csummary\u003EThe active target\u0027s label, resolved to a single-identifier property so the markup never\r\n\t/// interpolates a chained member read (which renders blank in several razor cases).\u003C/summary\u003E\r\n\tstring ActiveName =\u003E ActiveTarget?.DisplayName ?? \u0022\u0022;\r\n\r\n\t/// \u003Csummary\u003EThe sub-line under the tabs: what the numbers below are measured against. This is the one\r\n\t/// piece of context that makes a baked offset readable later, so the panel shows it while you drag.\u003C/summary\u003E\r\n\tstring FrameNote\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar frame = ActiveTarget?.FrameName;\r\n\t\t\treturn string.IsNullOrEmpty( frame ) ? \u0022local offset \u00B7 edited live\u0022 : $\u0022{frame} \u00B7 edited live\u0022;\r\n\t\t}\r\n\t}\r\n\r\n\tvoid SelectTab( int idx )\r\n\t{\r\n\t\tActiveIndex = idx;\r\n\t\t_copyLabel = \u0022Copy\u0022;   // a stale \u0022Copied!\u0022 on a different target reads as a lie\r\n\t}\r\n\r\n\tvoid ClosePanel()\r\n\t{\r\n\t\tPanelOpen = false;\r\n\t\t_copyLabel = \u0022Copy\u0022;\r\n\t}\r\n\r\n\t// ---- field model ----\r\n\tpublic enum Field { PosX, PosY, PosZ, Pitch, Yaw, Roll, Scale }\r\n\r\n\tstruct Row { public Field field; public string label; public string fmt; public float step; public float min; public float max; public bool header; }\r\n\r\n\t/// \u003Csummary\u003EThe seven rows for the ACTIVE target, bounded by that target\u0027s TweakRanges. Built per read\r\n\t/// rather than held in a static array, because an accessory and a scene prop want very different\r\n\t/// ranges out of the same three sliders.\u003C/summary\u003E\r\n\tList\u003CRow\u003E Rows\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar g = ActiveTarget?.Ranges ?? TweakRanges.Accessory;\r\n\t\t\tfloat p = MathF.Max( g.PositionRange, 0.001f );\r\n\t\t\treturn new List\u003CRow\u003E\r\n\t\t\t{\r\n\t\t\t\tnew Row { field = Field.PosX,  label = \u0022Pos X\u0022,   fmt = \u00220.###\u0022, step = g.PositionStep, min = -p, max = p },\r\n\t\t\t\tnew Row { field = Field.PosY,  label = \u0022Pos Y\u0022,   fmt = \u00220.###\u0022, step = g.PositionStep, min = -p, max = p },\r\n\t\t\t\tnew Row { field = Field.PosZ,  label = \u0022Pos Z\u0022,   fmt = \u00220.###\u0022, step = g.PositionStep, min = -p, max = p },\r\n\t\t\t\tnew Row { field = Field.Pitch, label = \u0022Pitch \u00B0\u0022, fmt = \u00220.#\u0022,   step = g.RotationStep, min = -180f, max = 180f, header = true },\r\n\t\t\t\tnew Row { field = Field.Yaw,   label = \u0022Yaw \u00B0\u0022,   fmt = \u00220.#\u0022,   step = g.RotationStep, min = -180f, max = 180f },\r\n\t\t\t\tnew Row { field = Field.Roll,  label = \u0022Roll \u00B0\u0022,  fmt = \u00220.#\u0022,   step = g.RotationStep, min = -180f, max = 180f },\r\n\t\t\t\tnew Row { field = Field.Scale, label = \u0022Scale\u0022,   fmt = \u00220.###\u0022, step = g.ScaleStep,    min = g.ScaleMin, max = g.ScaleMax, header = true },\r\n\t\t\t};\r\n\t\t}\r\n\t}\r\n\r\n\tfloat StepMult =\u003E _step switch { StepSize.Coarse =\u003E 10f, StepSize.XFine =\u003E 0.1f, _ =\u003E 1f };\r\n\r\n\tstatic float Frac( float value, Row row )\r\n\t\t=\u003E Math.Clamp( (value - row.min) / MathF.Max( row.max - row.min, 0.0001f ), 0f, 1f );\r\n\r\n\t// ---- read/write the active target\u0027s LOCAL transform ----\r\n\tstatic float Get( ITweakTarget t, Field f )\r\n\t{\r\n\t\tvar go = t?.Target;\r\n\t\tif ( go is null || !go.IsValid() ) return 0f;\r\n\t\tvar p = go.LocalPosition;\r\n\t\tvar a = go.LocalRotation.Angles();\r\n\t\treturn f switch\r\n\t\t{\r\n\t\t\tField.PosX =\u003E p.x,\r\n\t\t\tField.PosY =\u003E p.y,\r\n\t\t\tField.PosZ =\u003E p.z,\r\n\t\t\tField.Pitch =\u003E a.pitch,\r\n\t\t\tField.Yaw =\u003E a.yaw,\r\n\t\t\tField.Roll =\u003E a.roll,\r\n\t\t\tField.Scale =\u003E go.LocalScale.x,\r\n\t\t\t_ =\u003E 0f,\r\n\t\t};\r\n\t}\r\n\r\n\tvoid Nudge( Field f, float delta )\r\n\t{\r\n\t\tvar go = ActiveTarget?.Target;\r\n\t\tif ( go is null || !go.IsValid() ) return;\r\n\t\tvar p = go.LocalPosition;\r\n\t\tvar a = go.LocalRotation.Angles();\r\n\t\tswitch ( f )\r\n\t\t{\r\n\t\t\tcase Field.PosX: go.LocalPosition = p.WithX( p.x \u002B delta ); break;\r\n\t\t\tcase Field.PosY: go.LocalPosition = p.WithY( p.y \u002B delta ); break;\r\n\t\t\tcase Field.PosZ: go.LocalPosition = p.WithZ( p.z \u002B delta ); break;\r\n\t\t\tcase Field.Pitch: go.LocalRotation = new Angles( a.pitch \u002B delta, a.yaw, a.roll ).ToRotation(); break;\r\n\t\t\tcase Field.Yaw: go.LocalRotation = new Angles( a.pitch, a.yaw \u002B delta, a.roll ).ToRotation(); break;\r\n\t\t\tcase Field.Roll: go.LocalRotation = new Angles( a.pitch, a.yaw, a.roll \u002B delta ).ToRotation(); break;\r\n\t\t\tcase Field.Scale:\r\n\t\t\t\tfloat s = MathF.Max( 0.01f, go.LocalScale.x \u002B delta );\r\n\t\t\t\tgo.LocalScale = new Vector3( s, s, s );\r\n\t\t\t\tbreak;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EDraggable track (World Builder left-UI idiom): onmousedown JUMPS to the click, onmousemove\r\n\t/// SCRUBS while Active. Snaps to the row\u0027s fine step, applies as a DELTA through Nudge so the same\r\n\t/// write path runs whether you drag or step.\u003C/summary\u003E\r\n\tvoid TrackPointer( PanelEvent ev, Row row, bool jump )\r\n\t{\r\n\t\tif ( ev is not MousePanelEvent e ) return;\r\n\t\tvar track = e.This;\r\n\t\tif ( track is null ) return;\r\n\t\tif ( !jump \u0026\u0026 !track.PseudoClass.HasFlag( PseudoClass.Active ) ) return;\r\n\r\n\t\tfloat w = track.Box.Rect.Width;\r\n\t\tif ( w \u003C= 0f ) return;\r\n\t\tfloat frac = Math.Clamp( e.LocalPosition.x / w, 0f, 1f );\r\n\t\tfloat target = row.min \u002B frac * (row.max - row.min);\r\n\t\tif ( row.step \u003E 0f ) target = MathF.Round( target / row.step ) * row.step;\r\n\t\ttarget = Math.Clamp( target, row.min, row.max );\r\n\t\tNudge( row.field, target - Get( ActiveTarget, row.field ) );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ERestore the transform the target was REGISTERED with, not zero. Zeroing a hand-mounted\r\n\t/// accessory collapses it into the wrist, which is never the thing you wanted back.\u003C/summary\u003E\r\n\tvoid ResetActive()\r\n\t{\r\n\t\tvar go = ActiveTarget?.Target;\r\n\t\tif ( go is null || !go.IsValid() ) return;\r\n\t\tif ( TweakSession.Instance?.ResetToSeed( go ) == true ) return;\r\n\r\n\t\t// Never registered here (a hand-built target list, say): identity is the only baseline we have.\r\n\t\tgo.LocalPosition = Vector3.Zero;\r\n\t\tgo.LocalRotation = Rotation.Identity;\r\n\t\tgo.LocalScale = Vector3.One;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ECopy the active target\u0027s paste-ready bake line to the system clipboard. Game-side\r\n\t/// Sandbox.UI.Clipboard.SetText, so this works in play without an editor round trip.\u003C/summary\u003E\r\n\tvoid CopyActive()\r\n\t{\r\n\t\tvar t = ActiveTarget;\r\n\t\tif ( t is null ) return;\r\n\t\tvar line = PlacementExport.BakeLine( t );\r\n\t\tif ( string.IsNullOrEmpty( line ) ) return;\r\n\t\tSandbox.UI.Clipboard.SetText( line );\r\n\t\t_copyLabel = \u0022Copied!\u0022;\r\n\t}\r\n\r\n\tvoid ExportNow()\r\n\t{\r\n\t\tif ( Scene is not null )\r\n\t\t\tPlacementExport.WriteAll( Scene );\r\n\t}\r\n\r\n\t// ---- boot state, P toggle, cursor while open ----\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\t// BOOT. \u0060placement_panel\u0060 is a convar and s\u0026box PERSISTS convars across sessions, so a session can\r\n\t\t// otherwise come up with the panel logically open from whatever someone left set weeks ago. The rule\r\n\t\t// that prevents it: this component\u0027s own OpenOnStart decides the boot state, and the persisted value\r\n\t\t// never does. Default off means a consumer game still cannot be pre-opened by a stale convar; a scene\r\n\t\t// that wants the panel up says so explicitly.\r\n\t\t//\r\n\t\t// Deliberately in the FIRST UPDATE rather than OnStart. A panel built in code is configured by the\r\n\t\t// component that created it, and doing this in OnStart would race that assignment: whichever ran\r\n\t\t// first would win. The first update is after every OnStart in the frame, so the setting is always\r\n\t\t// read, never half-applied.\r\n\t\tif ( !_booted )\r\n\t\t{\r\n\t\t\t_booted = true;\r\n\t\t\tif ( PanelOpen \u0026\u0026 !OpenOnStart )\r\n\t\t\t\tLog.Info( \u0022[placement] tweak panel was OPEN at session start (persisted convar), forcing closed\u0022 );\r\n\t\t\tPanelOpen = OpenOnStart;\r\n\t\t}\r\n\r\n\t\tif ( Input.Keyboard.Pressed( \u0022P\u0022 ) )\r\n\t\t\tPanelOpen = !PanelOpen;\r\n\r\n\t\tif ( PanelOpen )\r\n\t\t{\r\n\t\t\tMouse.Visibility = MouseVisibility.Visible;   // keep the cursor usable over the panel\r\n\t\t\t_wasOpen = true;\r\n\t\t}\r\n\t\telse if ( _wasOpen )\r\n\t\t{\r\n\t\t\t_wasOpen = false;\r\n\t\t\t_copyLabel = \u0022Copy\u0022;   // closing clears the flash, so a reopen never claims a copy that was not made\r\n\t\t}\r\n\t}\r\n\r\n\t// Fold the toggle, active tab, step mode, the copy label, and every displayed value (rounded) so\r\n\t// readouts update the instant a value is nudged. Miss one and the number freezes on screen.\r\n\tprotected override int BuildHash()\r\n\t{\r\n\t\tint h = HashCode.Combine( PanelOpen, ActiveIndex, (int)_step, _copyLabel, TargetList.Count );\r\n\t\tvar a = ActiveTarget;\r\n\t\tif ( a is not null )\r\n\t\t{\r\n\t\t\th = HashCode.Combine( h, a.FrameName );\r\n\t\t\tforeach ( var r in Rows )\r\n\t\t\t\th = HashCode.Combine( h, (int)MathF.Round( Get( a, r.field ) * 1000f ) );\r\n\t\t}\r\n\t\treturn h;\r\n\t}\r\n}\r\n"}]}