{"TotalCount":483,"Files":[{"Ident":"pooh.geppetto","Path":"Editor/Effigy/DmxAnimWriter.cs","FileName":"DmxAnimWriter.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":411088,"IsPrivate":false,"Code":"using System;\nusing System.IO;\n\nnamespace Effigy;\n\n/// \u003Csummary\u003E\n/// DMX export \u2014 the animation path. A skeleton and a set of per-bone channels, which is what\n/// ModelDoc\u0027s \u0060AnimFile\u0060 node wants pointed at it, and therefore what puts a hand-authored clip\n/// inside a compiled model where AnimGraph can reach it.\n///\n/// WHY THIS IS NOT SMD, WHICH IS THE OBVIOUS ANSWER AND THE WRONG ONE. A sequence SMD is the\n/// classic way to hand-write animation for a Source model, SmdWriter already emits the exact\n/// \u0060skeleton\u0060 / \u0060time N\u0060 / bone-row block a sequence needs, and extending it looks like an\n/// afternoon\u0027s work. ModelDoc does not read SMD at all \u2014 see DmxWriter\u0027s header for the loader\n/// string that says so in the compiler\u0027s own words. The mesh path learned that the expensive way;\n/// this file exists so the animation path does not learn it again.\n///\n/// COPIED, NOT GUESSED \u2014 and there is a command that produces the thing to copy. The engine ships\n/// \u0060bin/win64/fbx2dmx.exe\u0060, whose \u003Cb\u003E-a\u003C/b\u003E flag converts animation rather than geometry, and\n/// every element, attribute and spelling below was read off its output for a shipping clip:\n///\n///   fbx2dmx.exe -a -i addons/citizen/Assets/models/citizen/animations/face/Citizen@Eyes_Blink.fbx -o ref.dmx\n///\n/// That reference is the only evidence there is about this format, and regenerating it beats\n/// reasoning from this comment if anything here ever stops working. What it settled, none of which\n/// is inferable from the element names alone:\n///\n///   - \u0060animationList\u0060 hangs off the ROOT DmElement, beside \u0060skeleton\u0060 and \u0060model\u0060 \u2014 not inside\n///     the DmeModel, which is where it looks like it should go;\n///   - a channel targets the bone\u0027s DmeTransform, not its DmeJoint, and does it by id;\n///   - each bone needs TWO channels, suffixed \u0060_p\u0060 and \u0060_o\u0060, writing \u0060position\u0060 and \u0060orientation\u0060\n///     respectively. One channel per bone carrying both does not exist;\n///   - \u0060mode\u0060 is 3 on every channel fbx2dmx writes;\n///   - the log layer\u0027s \u0060curvetypes\u0060 array is present and EMPTY, which is what \u0022no per-key curve\n///     override\u0022 looks like. Omitting the array is a different statement.\n///\n/// WHAT IS DELIBERATELY LEFT OUT. The reference also carries a \u0060compressed\u0060 binary blob on each\n/// log layer \u2014 empty in every layer of it. An empty blob says nothing its absence does not, and\n/// KeyValues2\u0027s binary literal is a multi-line quoted form with no second example to check a guess\n/// against, so this writes no \u0060compressed\u0060 attribute rather than an invented one.\n/// \u0060dmxconvert.exe -i clip.dmx -o check.dmx\u0060 is the one-second check that this was the right call;\n/// DmxAnimTests runs the same parse with no engine involved.\n/// \u003C/summary\u003E\npublic static class DmxAnimWriter\n{\n\tpublic static void WriteFile( string path, Skeleton skeleton, AnimClip clip, string modelName = null )\n\t{\n\t\tFile.WriteAllText( path, Write( skeleton, clip, modelName ) );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The animation as a DMX document.\n\t///\n\t/// The skeleton written here has to be the SAME skeleton the mesh was exported with. ModelDoc\n\t/// matches a clip\u0027s channels to a model\u0027s bones by name, and a bone the clip poses that the\n\t/// model does not have is dropped silently \u2014 so a rig edited between the two exports gives you\n\t/// a clip that compiles, loads, and moves less of the model than it used to.\n\t/// \u003C/summary\u003E\n\tpublic static string Write( Skeleton skeleton, AnimClip clip, string modelName = null )\n\t{\n\t\tif ( skeleton is null )\n\t\t\tthrow new ArgumentNullException( nameof( skeleton ) );\n\n\t\tif ( clip is null )\n\t\t\tthrow new ArgumentNullException( nameof( clip ) );\n\n\t\tif ( clip.Validate( skeleton ) is { } problem )\n\t\t\tthrow new InvalidOperationException( $\u0022Animation clip does not fit its skeleton: {problem}\u0022 );\n\n\t\tmodelName ??= \u0022effigy_model\u0022;\n\n\t\tvar w = new DmxText();\n\n\t\t// Same id discipline as the mesh writer: counted, not random, so two exports of the same\n\t\t// clip are byte-identical and a diff shows what actually changed.\n\t\tvar idRoot = w.NextId();\n\t\tvar idModel = w.NextId();\n\t\tvar idModelTransform = w.NextId();\n\n\t\tvar boneDagIds = new string[skeleton.Count];\n\t\tvar boneTransformIds = new string[skeleton.Count];\n\t\tvar bindTransformIds = new string[skeleton.Count];\n\n\t\tfor ( var i = 0; i \u003C skeleton.Count; i\u002B\u002B )\n\t\t{\n\t\t\tboneDagIds[i] = w.NextId();\n\t\t\tboneTransformIds[i] = w.NextId();\n\t\t\tbindTransformIds[i] = w.NextId();\n\t\t}\n\n\t\tvar idAnimList = w.NextId();\n\t\tvar idClip = w.NextId();\n\t\tvar idTimeFrame = w.NextId();\n\n\t\tw.Raw( $\u0022\u003C!-- dmx encoding keyvalues2 1 format model {DmxWriter.ModelFormatVersion} --\u003E\u0022 );\n\t\tw.Raw( \u0022\u0022 );\n\n\t\tw.OpenElement( \u0022DmElement\u0022, idRoot, \u0022root\u0022 );\n\n\t\t// No mesh child: an animation DMX carries the rig and the motion, and the geometry lives in\n\t\t// the model file this clip gets compiled into.\n\t\tDmxWriter.WriteSkeletonModel( w, skeleton, modelName, idModel, idModelTransform,\n\t\t\tboneDagIds, boneTransformIds, bindTransformIds, null );\n\n\t\tw.Attribute( \u0022model\u0022, \u0022element\u0022, idModel );\n\n\t\tWriteAnimationList( w, skeleton, clip, idAnimList, idClip, idTimeFrame, boneTransformIds );\n\n\t\tw.CloseElement();\n\n\t\treturn w.ToString();\n\t}\n\n\t// --- pieces -------------------------------------------------------------------------------\n\n\tstatic void WriteAnimationList( DmxText w, Skeleton skeleton, AnimClip clip,\n\t\tstring idAnimList, string idClip, string idTimeFrame, string[] boneTransformIds )\n\t{\n\t\tw.OpenAttributeElement( \u0022animationList\u0022, \u0022DmeAnimationList\u0022, idAnimList, \u0022anim\u0022 );\n\t\tw.OpenArray( \u0022animations\u0022, \u0022element_array\u0022 );\n\n\t\tw.OpenArrayElement( \u0022DmeChannelsClip\u0022, idClip, clip.Name );\n\t\t{\n\t\t\tw.OpenAttributeElement( \u0022timeFrame\u0022, \u0022DmeTimeFrame\u0022, idTimeFrame, \u0022timeFrame\u0022 );\n\t\t\tw.Attribute( \u0022start\u0022, \u0022time\u0022, DmxText.Time( 0f ) );\n\t\t\tw.Attribute( \u0022duration\u0022, \u0022time\u0022, DmxText.Time( clip.Duration ) );\n\t\t\tw.Attribute( \u0022offset\u0022, \u0022time\u0022, DmxText.Time( 0f ) );\n\t\t\tw.Attribute( \u0022scale\u0022, \u0022float\u0022, \u00221\u0022 );\n\t\t\tw.CloseElement();\n\n\t\t\tw.OpenArray( \u0022channels\u0022, \u0022element_array\u0022 );\n\n\t\t\t// Two channels per bone, position then orientation, in skeleton order. The order is not\n\t\t\t// load-bearing \u2014 channels name their own target \u2014 but keeping it stable is what makes\n\t\t\t// two exports of the same clip diffable.\n\t\t\tfor ( var i = 0; i \u003C skeleton.Count; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tWritePositionChannel( w, skeleton, clip, i, boneTransformIds[i] );\n\t\t\t\tWriteOrientationChannel( w, skeleton, clip, i, boneTransformIds[i] );\n\t\t\t}\n\n\t\t\tw.CloseArray();\n\t\t}\n\t\tw.CloseElement();\n\n\t\tw.CloseArray();\n\t\tw.CloseElement();\n\t}\n\n\tstatic void WritePositionChannel( DmxText w, Skeleton skeleton, AnimClip clip, int bone, string targetId )\n\t{\n\t\tw.OpenArrayElement( \u0022DmeChannel\u0022, w.NextId(), $\u0022{skeleton.Bones[bone].Name}_p\u0022 );\n\t\tWriteChannelTarget( w, targetId, \u0022position\u0022 );\n\n\t\tw.OpenAttributeElement( \u0022log\u0022, \u0022DmeVector3Log\u0022, w.NextId(), \u0022vector3 log\u0022 );\n\t\tw.OpenArray( \u0022layers\u0022, \u0022element_array\u0022 );\n\t\tw.OpenArrayElement( \u0022DmeVector3LogLayer\u0022, w.NextId(), \u0022vector3 log\u0022 );\n\n\t\tWriteTimes( w, clip );\n\n\t\tw.OpenArray( \u0022values\u0022, \u0022vector3_array\u0022 );\n\n\t\tfor ( var f = 0; f \u003C clip.FrameCount; f\u002B\u002B )\n\t\t\tw.ArrayValue( DmxText.Vector3( clip.Frames[f][bone].Origin ) );\n\n\t\tw.CloseArray();\n\t\tw.CloseElement();\n\t\tw.CloseArray();\n\n\t\tw.Attribute( \u0022usedefaultvalue\u0022, \u0022bool\u0022, \u00220\u0022 );\n\t\tw.Attribute( \u0022defaultvalue\u0022, \u0022vector3\u0022, \u00220 0 0\u0022 );\n\t\tw.CloseElement();\n\n\t\tw.CloseElement();\n\t}\n\n\tstatic void WriteOrientationChannel( DmxText w, Skeleton skeleton, AnimClip clip, int bone, string targetId )\n\t{\n\t\tw.OpenArrayElement( \u0022DmeChannel\u0022, w.NextId(), $\u0022{skeleton.Bones[bone].Name}_o\u0022 );\n\t\tWriteChannelTarget( w, targetId, \u0022orientation\u0022 );\n\n\t\tw.OpenAttributeElement( \u0022log\u0022, \u0022DmeQuaternionLog\u0022, w.NextId(), \u0022quaternion log\u0022 );\n\t\tw.OpenArray( \u0022layers\u0022, \u0022element_array\u0022 );\n\t\tw.OpenArrayElement( \u0022DmeQuaternionLogLayer\u0022, w.NextId(), \u0022quaternion log\u0022 );\n\n\t\tWriteTimes( w, clip );\n\n\t\tw.OpenArray( \u0022values\u0022, \u0022quaternion_array\u0022 );\n\n\t\tfor ( var f = 0; f \u003C clip.FrameCount; f\u002B\u002B )\n\t\t\tw.ArrayValue( DmxText.Quaternion( clip.Frames[f][bone] ) );\n\n\t\tw.CloseArray();\n\t\tw.CloseElement();\n\t\tw.CloseArray();\n\n\t\tw.Attribute( \u0022usedefaultvalue\u0022, \u0022bool\u0022, \u00220\u0022 );\n\t\tw.Attribute( \u0022defaultvalue\u0022, \u0022quaternion\u0022, \u00220 0 0 1\u0022 );\n\t\tw.CloseElement();\n\n\t\tw.CloseElement();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The half of a channel that says where its values go.\n\t///\n\t/// \u0060fromElement\u0060 and \u0060fromAttribute\u0060 are empty because nothing drives this channel \u2014 it is a\n\t/// stored curve, not a connection between two live elements, which is the other thing a\n\t/// DmeChannel gets used for. \u0060mode\u0060 3 is what fbx2dmx writes on every channel of an exported\n\t/// clip; the enum it comes from is not in anything readable here, so this is copied rather than\n\t/// named.\n\t/// \u003C/summary\u003E\n\tstatic void WriteChannelTarget( DmxText w, string targetId, string attribute )\n\t{\n\t\tw.Attribute( \u0022fromElement\u0022, \u0022element\u0022, \u0022\u0022 );\n\t\tw.Attribute( \u0022fromAttribute\u0022, \u0022string\u0022, \u0022\u0022 );\n\t\tw.Attribute( \u0022fromIndex\u0022, \u0022int\u0022, \u00220\u0022 );\n\t\tw.Attribute( \u0022toElement\u0022, \u0022element\u0022, targetId );\n\t\tw.Attribute( \u0022toAttribute\u0022, \u0022string\u0022, attribute );\n\t\tw.Attribute( \u0022toIndex\u0022, \u0022int\u0022, \u00220\u0022 );\n\t\tw.Attribute( \u0022mode\u0022, \u0022int\u0022, \u00223\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The sample times, plus the empty curvetypes array that goes with them.\n\t///\n\t/// Written once for both channel kinds because the two have to agree exactly: a position log\n\t/// and an orientation log with different time arrays is a bone whose translation and rotation\n\t/// drift apart, which reads as a rigging fault rather than an export one.\n\t/// \u003C/summary\u003E\n\tstatic void WriteTimes( DmxText w, AnimClip clip )\n\t{\n\t\tw.OpenArray( \u0022times\u0022, \u0022time_array\u0022 );\n\n\t\tfor ( var f = 0; f \u003C clip.FrameCount; f\u002B\u002B )\n\t\t\tw.ArrayValue( DmxText.Time( clip.TimeOf( f ) ) );\n\n\t\tw.CloseArray();\n\n\t\t// Present and empty \u2014 see the header. This is \u0022no per-key curve override\u0022, not \u0022no curves\u0022.\n\t\tw.OpenArray( \u0022curvetypes\u0022, \u0022int_array\u0022 );\n\t\tw.CloseArray();\n\t}\n}\n"},{"Ident":"pooh.geppetto","Path":"Editor/Effigy/Features/BasicFeatures.cs","FileName":"BasicFeatures.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":411088,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace Effigy;\n\n/// \u003Csummary\u003E\n/// Creates a primitive solid. Onshape\u0027s own Primitives feature works this way \u2014 one dropdown, and\n/// the dialog shows only the fields that shape actually has.\n///\n/// Parameters deliberately changes with Shape rather than showing every field greyed out. That is\n/// the behaviour being copied: a box dialog asks for three lengths and nothing else.\n/// \u003C/summary\u003E\npublic sealed class PrimitiveFeature : Feature\n{\n\tpublic override string TypeName =\u003E Shape.Value;\n\n\tpublic readonly ChoiceParam Shape = new( \u0022Shape\u0022,\n\t\tnew[] { \u0022Box\u0022, \u0022Cylinder\u0022, \u0022Sphere\u0022, \u0022Wedge\u0022, \u0022Tube\u0022, \u0022Plane\u0022 } );\n\n\tpublic readonly FloatParam SizeX = new( \u0022Width\u0022, 1f, 0.0001f, unit: \u0022u\u0022 );\n\tpublic readonly FloatParam SizeY = new( \u0022Depth\u0022, 1f, 0.0001f, unit: \u0022u\u0022 );\n\tpublic readonly FloatParam SizeZ = new( \u0022Height\u0022, 1f, 0.0001f, unit: \u0022u\u0022 );\n\tpublic readonly FloatParam Radius = new( \u0022Radius\u0022, 0.5f, 0.0001f, unit: \u0022u\u0022 );\n\tpublic readonly FloatParam InnerRadius = new( \u0022Inner radius\u0022, 0.3f, 0f, unit: \u0022u\u0022 );\n\tpublic readonly IntParam Segments = new( \u0022Segments\u0022, 16, 3, 512 );\n\tpublic readonly IntParam Divisions = new( \u0022Divisions\u0022, 4, 1, 64 );\n\tpublic readonly Vec3Param Position = new( \u0022Position\u0022, Vec3.Zero );\n\n\t/// \u003Csummary\u003E\n\t/// Per-axis scale, applied about the primitive\u0027s own origin before it is moved into place.\n\t///\n\t/// NOT THE SAME THING AS THE SIZE PARAMETERS, and worth having alongside them. Width/Depth/\n\t/// Height build the shape at a size; this stretches whatever was built, which is the only way\n\t/// to get an ellipsoid out of a sphere or an oval tube out of a round one \u2014 those are defined\n\t/// by a radius and have no per-axis size to set.\n\t/// \u003C/summary\u003E\n\tpublic readonly Vec3Param Scale = new( \u0022Scale\u0022, Vec3.One );\n\n\t/// \u003Csummary\u003E\n\t/// Whether the dialog keeps the three scale axes equal as you edit one of them.\n\t///\n\t/// A UI CONVENIENCE THAT IS PERSISTED, not something the kernel enforces. Scale stays the\n\t/// single truth about the shape \u2014 Execute reads all three axes and never consults this \u2014 so a\n\t/// document always builds exactly what its three numbers say. This only rides along so the\n\t/// dialog can remember that you were editing that primitive uniformly.\n\t/// \u003C/summary\u003E\n\tpublic readonly BoolParam UniformScale = new( \u0022Uniform scale\u0022, false );\n\n\tpublic readonly IntParam Material = new( \u0022Material slot\u0022, 0, 0, 63 ) { Slider = false };\n\n\t/// \u003Csummary\u003EA turn about an axis through the primitive\u0027s own origin, applied before Position \u2014\n\t/// a cylinder lying along x is an axle, a wedge on its side is a roof. Folded away: most\n\t/// primitives stand as they come.\u003C/summary\u003E\n\tpublic readonly Vec3Param RotationAxis = new( \u0022Rotation axis\u0022, new Vec3( 0, 0, 1 ) );\n\tpublic readonly FloatParam RotationAngle = new( \u0022Rotation\u0022, 0f, unit: \u0022deg\u0022 );\n\n\t/// \u003Csummary\u003EThe slot, folded away with the other features\u0027 \u2014 a primitive is placed and sized,\n\t/// and painted later from the Materials panel.\u003C/summary\u003E\n\tpublic override IReadOnlyList\u003CIParam\u003E AdvancedParameters =\u003E new IParam[] { RotationAxis, RotationAngle, Material };\n\n\tpublic override IReadOnlyList\u003CIParam\u003E Parameters =\u003E Shape.Value switch\n\t{\n\t\t\u0022Box\u0022 =\u003E new IParam[] { Shape, SizeX, SizeY, SizeZ, Position, Scale, UniformScale, RotationAxis, RotationAngle, Material },\n\t\t\u0022Cylinder\u0022 =\u003E new IParam[] { Shape, Radius, SizeZ, Segments, Position, Scale, UniformScale, RotationAxis, RotationAngle, Material },\n\t\t\u0022Sphere\u0022 =\u003E new IParam[] { Shape, Radius, Divisions, Position, Scale, UniformScale, RotationAxis, RotationAngle, Material },\n\t\t\u0022Wedge\u0022 =\u003E new IParam[] { Shape, SizeX, SizeY, SizeZ, Position, Scale, UniformScale, RotationAxis, RotationAngle, Material },\n\t\t\u0022Tube\u0022 =\u003E new IParam[] { Shape, Radius, InnerRadius, SizeZ, Segments, Position, Scale, UniformScale, RotationAxis, RotationAngle, Material },\n\t\t\u0022Plane\u0022 =\u003E new IParam[] { Shape, SizeX, SizeY, Segments, Position, Scale, UniformScale, RotationAxis, RotationAngle, Material },\n\t\t_ =\u003E new IParam[] { Shape }\n\t};\n\n\tprotected override void Execute( FeatureContext ctx )\n\t{\n\t\tvar mesh = Shape.Value switch\n\t\t{\n\t\t\t\u0022Box\u0022 =\u003E Primitives.Box( SizeX.Clamped, SizeY.Clamped, SizeZ.Clamped, Material.Clamped ),\n\t\t\t\u0022Cylinder\u0022 =\u003E Primitives.Cylinder( Radius.Clamped, SizeZ.Clamped, Segments.Clamped, Material.Clamped ),\n\t\t\t\u0022Sphere\u0022 =\u003E Primitives.QuadSphere( Radius.Clamped, Divisions.Clamped, Material.Clamped ),\n\t\t\t\u0022Wedge\u0022 =\u003E Primitives.Wedge( SizeX.Clamped, SizeY.Clamped, SizeZ.Clamped, Material.Clamped ),\n\t\t\t\u0022Tube\u0022 =\u003E BuildTube(),\n\t\t\t\u0022Plane\u0022 =\u003E Primitives.Plane( SizeX.Clamped, SizeY.Clamped, Segments.Clamped, Segments.Clamped, Material.Clamped ),\n\t\t\t_ =\u003E throw new FeatureException( new FeatureDiagnostic(\n\t\t\t\tDiagnosticSeverity.Error,\n\t\t\t\t$\u0022unknown shape \u0027{Shape.Value}\u0027\u0022,\n\t\t\t\t\u0022The shape dropdown has a value this feature does not build.\u0022,\n\t\t\t\t\u0022Shape\u0022,\n\t\t\t\tremedies: new[] { \u0022Pick Box, Cylinder, Sphere, Wedge, Tube or Plane\u0022 } ) )\n\t\t};\n\n\t\tvar scale = Scale.Value;\n\n\t\tif ( scale.x == 0f || scale.y == 0f || scale.z == 0f )\n\t\t{\n\t\t\tFailOn( \u0022Scale\u0022,\n\t\t\t\t\u0022Scale cannot be zero on any axis\u0022,\n\t\t\t\t$\u0022Scale is ({scale.x:0.###}, {scale.y:0.###}, {scale.z:0.###}). A zero axis flattens the solid to nothing.\u0022,\n\t\t\t\t\u0022Set every scale axis to a non-zero value\u0022 );\n\t\t}\n\n\t\t// SCALE FIRST, ABOUT THE PRIMITIVE\u0027S OWN ORIGIN. Applied after the translate it would\n\t\t// multiply the position too, so nudging a scaled box would move it by the scale factor and\n\t\t// the number in the Position field would stop meaning where the box is.\n\t\tif ( scale.x != 1f || scale.y != 1f || scale.z != 1f )\n\t\t\tMeshTransform.Apply( mesh, Xform.Scale( scale ) );\n\n\t\tif ( RotationAngle.Value != 0f \u0026\u0026 RotationAxis.Value.LengthSquared \u003E 1e-8f )\n\t\t\tMeshTransform.Apply( mesh, Xform.Rotate( RotationAxis.Value.Normal, RotationAngle.Value * MathF.PI / 180f ) );\n\n\t\tif ( Position.Value.LengthSquared \u003E 0f )\n\t\t\tMeshTransform.Apply( mesh, Xform.Translate( Position.Value ) );\n\n\t\tctx.Bodies.Add( new Body( ctx.NewBodyId(), Name, mesh ) );\n\t}\n\n\tPolyMesh BuildTube()\n\t{\n\t\t// Caught here rather than left to Primitives, so the message names the parameter the user\n\t\t// can actually see in the dialog.\n\t\tif ( InnerRadius.Clamped \u003E= Radius.Clamped )\n\t\t{\n\t\t\tFailOn( \u0022Inner radius\u0022,\n\t\t\t\t\u0022Inner radius must be smaller than radius\u0022,\n\t\t\t\t$\u0022Inner radius is {InnerRadius.Clamped:0.###} and radius is {Radius.Clamped:0.###}.\u0022,\n\t\t\t\t\u0022Reduce Inner radius\u0022,\n\t\t\t\t\u0022Increase Radius\u0022 );\n\t\t}\n\n\t\treturn Primitives.Tube( Radius.Clamped, InnerRadius.Clamped, SizeZ.Clamped, Segments.Clamped, Material.Clamped );\n\t}\n}\n\n/// \u003Csummary\u003EMove, rotate and scale bodies. Onshape\u0027s Transform.\u003C/summary\u003E\npublic sealed class TransformFeature : Feature\n{\n\tpublic override string TypeName =\u003E \u0022Transform\u0022;\n\n\tpublic override GeometryKind Accepts =\u003E GeometryKind.Body;\n\n\tpublic readonly BodySelectionParam Bodies = new( \u0022Bodies\u0022 );\n\tpublic readonly Vec3Param Translate = new( \u0022Translate\u0022, Vec3.Zero );\n\tpublic readonly Vec3Param RotationAxis = new( \u0022Rotation axis\u0022, new Vec3( 0, 0, 1 ) );\n\tpublic readonly FloatParam RotationAngle = new( \u0022Angle\u0022, 0f, unit: \u0022deg\u0022 );\n\tpublic readonly Vec3Param Scale = new( \u0022Scale\u0022, Vec3.One );\n\n\tpublic override IReadOnlyList\u003CIParam\u003E Parameters =\u003E\n\t\tnew IParam[] { Bodies, Translate, RotationAxis, RotationAngle, Scale };\n\n\tprotected override void Execute( FeatureContext ctx )\n\t{\n\t\tvar scale = Scale.Value;\n\n\t\tif ( scale.x == 0f || scale.y == 0f || scale.z == 0f )\n\t\t{\n\t\t\tFailOn( \u0022Scale\u0022,\n\t\t\t\t\u0022Scale cannot be zero on any axis\u0022,\n\t\t\t\t$\u0022Scale is ({scale.x:0.###}, {scale.y:0.###}, {scale.z:0.###}). A zero axis flattens the solid to nothing.\u0022,\n\t\t\t\t\u0022Set every scale axis to a non-zero value\u0022 );\n\t\t}\n\n\t\t// Scale, then rotate, then translate \u2014 the order a user expects, and the one that keeps a\n\t\t// rotation about the origin from being skewed by a non-uniform scale applied after it.\n\t\tvar xform =\n\t\t\tXform.Translate( Translate.Value )\n\t\t\t* Xform.Rotate( RotationAxis.Value, RotationAngle.Value * MathF.PI / 180f )\n\t\t\t* Xform.Scale( scale );\n\n\t\tforeach ( var body in RequireBodies( ctx, Bodies ) )\n\t\t\tMeshTransform.Apply( body.Mesh, xform );\n\t}\n}\n\n/// \u003Csummary\u003ECopies along a direction. Onshape\u0027s Linear pattern.\u003C/summary\u003E\npublic sealed class LinearPatternFeature : Feature\n{\n\tpublic override string TypeName =\u003E \u0022Linear pattern\u0022;\n\n\tpublic override GeometryKind Accepts =\u003E GeometryKind.Body;\n\n\tpublic readonly BodySelectionParam Bodies = new( \u0022Bodies\u0022 );\n\tpublic readonly Vec3Param Direction = new( \u0022Direction\u0022, new Vec3( 1, 0, 0 ) );\n\tpublic readonly FloatParam Spacing = new( \u0022Spacing\u0022, 1f, unit: \u0022u\u0022 );\n\tpublic readonly IntParam Count = new( \u0022Instances\u0022, 3, 1, 4096 );\n\tpublic readonly BoolParam Merge = new( \u0022Merge into one body\u0022, false );\n\n\tpublic override IReadOnlyList\u003CIParam\u003E Parameters =\u003E\n\t\tnew IParam[] { Bodies, Direction, Spacing, Count, Merge };\n\n\tprotected override void Execute( FeatureContext ctx )\n\t{\n\t\tif ( Direction.Value.LengthSquared \u003C 1e-12f )\n\t\t{\n\t\t\tFailOn( \u0022Direction\u0022,\n\t\t\t\t\u0022Direction cannot be zero\u0022,\n\t\t\t\t\u0022A linear pattern copies along a direction, and this one has no length, so there is nowhere to put the copies.\u0022,\n\t\t\t\t\u0022Set Direction to the axis you want the copies to run along\u0022 );\n\t\t}\n\n\t\tvar dir = Direction.Value.Normal;\n\t\tvar sources = RequireBodies( ctx, Bodies );\n\n\t\t// Instance 0 is the original, so a count of 3 means the original plus two copies \u2014 which\n\t\t// is what Onshape\u0027s instance count means too.\n\t\tforeach ( var source in sources )\n\t\t{\n\t\t\t// Snapshot BEFORE the loop. With Merge on, the loop appends into source.Mesh, so\n\t\t\t// reading source.Mesh each iteration would copy the copies too and the instance count\n\t\t\t// would double rather than increment: 6, 12, 24, 48 faces instead of 6, 12, 18, 24.\n\t\t\tvar original = source.Mesh.Clone();\n\n\t\t\tfor ( var i = 1; i \u003C Count.Clamped; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar copy = MeshTransform.Transformed( original, Xform.Translate( dir * (Spacing.Value * i) ) );\n\n\t\t\t\tif ( Merge.Value )\n\t\t\t\t\tMeshTransform.Append( source.Mesh, copy );\n\t\t\t\telse\n\t\t\t\t\tctx.Bodies.Add( new Body( ctx.NewBodyId(), $\u0022{Name} {i}\u0022, copy ) );\n\t\t\t}\n\t\t}\n\t}\n}\n\n/// \u003Csummary\u003ECopies around an axis. Onshape\u0027s Circular pattern.\u003C/summary\u003E\npublic sealed class CircularPatternFeature : Feature\n{\n\tpublic override string TypeName =\u003E \u0022Circular pattern\u0022;\n\n\tpublic override GeometryKind Accepts =\u003E GeometryKind.Body;\n\n\tpublic readonly BodySelectionParam Bodies = new( \u0022Bodies\u0022 );\n\tpublic readonly Vec3Param AxisPoint = new( \u0022Axis through\u0022, Vec3.Zero );\n\tpublic readonly Vec3Param AxisDirection = new( \u0022Axis\u0022, new Vec3( 0, 0, 1 ) );\n\tpublic readonly IntParam Count = new( \u0022Instances\u0022, 4, 1, 4096 );\n\tpublic readonly FloatParam TotalAngle = new( \u0022Angle\u0022, 360f, unit: \u0022deg\u0022 );\n\tpublic readonly BoolParam Merge = new( \u0022Merge into one body\u0022, false );\n\n\tpublic override IReadOnlyList\u003CIParam\u003E Parameters =\u003E\n\t\tnew IParam[] { Bodies, AxisPoint, AxisDirection, Count, TotalAngle, Merge };\n\n\tprotected override void Execute( FeatureContext ctx )\n\t{\n\t\tif ( AxisDirection.Value.LengthSquared \u003C 1e-12f )\n\t\t{\n\t\t\tFailOn( \u0022Axis\u0022,\n\t\t\t\t\u0022Axis cannot be zero\u0022,\n\t\t\t\t\u0022A circular pattern spins around an axis, and this one has no length.\u0022,\n\t\t\t\t\u0022Set Axis to the direction to spin around\u0022 );\n\t\t}\n\n\t\tvar sources = RequireBodies( ctx, Bodies );\n\n\t\tvar count = Count.Clamped;\n\n\t\t// A full turn puts instance N back on instance 0, so the step divides by count. A partial\n\t\t// sweep spreads the instances across the arc inclusive of both ends instead.\n\t\tvar full = MathF.Abs( MathF.Abs( TotalAngle.Value ) - 360f ) \u003C 1e-3f;\n\t\tvar step = count \u003C= 1 ? 0f : TotalAngle.Value / (full ? count : count - 1);\n\n\t\tforeach ( var source in sources )\n\t\t{\n\t\t\t// Snapshot before the loop \u2014 same compounding trap as the linear pattern.\n\t\t\tvar original = source.Mesh.Clone();\n\n\t\t\tfor ( var i = 1; i \u003C count; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar xform = Xform.RotateAbout(\n\t\t\t\t\tAxisPoint.Value, AxisDirection.Value, step * i * MathF.PI / 180f );\n\n\t\t\t\tvar copy = MeshTransform.Transformed( original, xform );\n\n\t\t\t\tif ( Merge.Value )\n\t\t\t\t\tMeshTransform.Append( source.Mesh, copy );\n\t\t\t\telse\n\t\t\t\t\tctx.Bodies.Add( new Body( ctx.NewBodyId(), $\u0022{Name} {i}\u0022, copy ) );\n\t\t\t}\n\t\t}\n\t}\n}\n\n/// \u003Csummary\u003EReflects bodies in a plane. Onshape\u0027s Mirror.\u003C/summary\u003E\npublic sealed class MirrorFeature : Feature\n{\n\tpublic override string TypeName =\u003E \u0022Mirror\u0022;\n\n\tpublic override GeometryKind Accepts =\u003E GeometryKind.Body;\n\n\tpublic readonly BodySelectionParam Bodies = new( \u0022Bodies\u0022 );\n\tpublic readonly Vec3Param PlanePoint = new( \u0022Plane through\u0022, Vec3.Zero );\n\tpublic readonly Vec3Param PlaneNormal = new( \u0022Plane normal\u0022, new Vec3( 1, 0, 0 ) );\n\tpublic readonly BoolParam KeepOriginal = new( \u0022Keep original\u0022, true );\n\tpublic readonly BoolParam Merge = new( \u0022Merge into one body\u0022, false );\n\n\tpublic override IReadOnlyList\u003CIParam\u003E Parameters =\u003E\n\t\tnew IParam[] { Bodies, PlanePoint, PlaneNormal, KeepOriginal, Merge };\n\n\tprotected override void Execute( FeatureContext ctx )\n\t{\n\t\tif ( PlaneNormal.Value.LengthSquared \u003C 1e-12f )\n\t\t{\n\t\t\tFailOn( \u0022Plane normal\u0022,\n\t\t\t\t\u0022Plane normal cannot be zero\u0022,\n\t\t\t\t\u0022A mirror needs a plane to reflect across, and a zero normal does not define one.\u0022,\n\t\t\t\t\u0022Set Plane normal to the direction the mirror should face\u0022 );\n\t\t}\n\n\t\tvar xform = Xform.Mirror( PlanePoint.Value, PlaneNormal.Value );\n\t\tvar sources = RequireBodies( ctx, Bodies );\n\n\t\tforeach ( var source in sources )\n\t\t{\n\t\t\t// MeshTransform.Apply reverses winding for us \u2014 the mirror flips handedness, and\n\t\t\t// without that reversal every mirrored face would point into the solid.\n\t\t\tvar copy = MeshTransform.Transformed( source.Mesh, xform );\n\n\t\t\tif ( Merge.Value )\n\t\t\t\tMeshTransform.Append( source.Mesh, copy );\n\t\t\telse\n\t\t\t\tctx.Bodies.Add( new Body( ctx.NewBodyId(), $\u0022{Name}\u0022, copy ) );\n\t\t}\n\n\t\tif ( !KeepOriginal.Value )\n\t\t{\n\t\t\tforeach ( var source in sources )\n\t\t\t\tctx.Bodies.Remove( source );\n\t\t}\n\t}\n}\n\n/// \u003Csummary\u003E\n/// Catmull-Clark subdivision as a history feature.\n///\n/// Onshape has no equivalent, and this is the deliberate place where the tool stops being CAD.\n/// Putting subdivision IN the tree rather than after it is what keeps the pipeline honest: roll\n/// the bar back above this feature and you are editing the low-poly cage, roll it forward and you\n/// see the dense surface. The cage is what the sculpt eventually bakes down onto, so it has to\n/// stay reachable rather than being consumed by an export step.\n/// \u003C/summary\u003E\npublic sealed class SubdivideFeature : Feature\n{\n\tpublic override string TypeName =\u003E \u0022Subdivide\u0022;\n\n\t/// \u003Csummary\u003EFaces as well as bodies: an empty Bodies list means the whole document to Execute,\n\t/// so picking is how you say which part - see the refusal in the editor\u0027s AddFeature.\u003C/summary\u003E\n\tpublic override GeometryKind Accepts =\u003E GeometryKind.Face | GeometryKind.Body;\n\n\t/// \u003Csummary\u003E\n\t/// Which faces to subdivide. EMPTY MEANS WHOLE BODY OR ALL FACES, depending on \u003Csee cref=\u0022AllFaces\u0022/\u003E,\n\t/// and the whole-body form is the old behaviour and the honest default \u2014 a subdivision surface\n\t/// is a property of a cage, not of a corner of one.\n\t///\n\t/// Picking faces switches the operation from smooth to linear, and the two really are different\n\t/// operations rather than a flag on one. See CatmullClark.SubdivideFaces: you cannot apply the\n\t/// limit rules to part of a mesh without moving the vertices the rest of it is standing on. So\n\t/// this is density where you need it \u2014 a face about to be sculpted, a panel about to be bent \u2014\n\t/// and the whole-body form remains the one that smooths.\n\t///\n\t/// Held as FaceRefs, like every other face pick, so the choice survives the rebuild that\n\t/// recreates the faces it names.\n\t/// \u003C/summary\u003E\n\tpublic List\u003CFaceRef\u003E Faces = new();\n\n\tpublic readonly BodySelectionParam Bodies = new( \u0022Bodies\u0022 );\n\tpublic readonly IntParam Levels = new( \u0022Levels\u0022, 1, 0, 6 );\n\n\t/// \u003Csummary\u003E\n\t/// Subdivide EVERY face, not the whole body. Only read when \u003Csee cref=\u0022Faces\u0022/\u003E is empty \u2014 a\n\t/// picked face already names its region, and a pick is more specific than \u0022everything\u0022.\n\t///\n\t/// The whole-body form (this off, no picks) runs the full Catmull-Clark and SMOOTHS: it moves\n\t/// the original vertices onto the limit surface. This is the density version of the same idea \u2014\n\t/// midpoints and centroids, so the shape stays exactly where it is and every face just gets\n\t/// more polygons. You want it when the whole cage is too coarse but the silhouette is already\n\t/// right, and it is why it is a tick rather than \u0022pick 40 faces\u0022.\n\t/// \u003C/summary\u003E\n\tpublic readonly BoolParam AllFaces = new( \u0022All faces\u0022, false );\n\n\tpublic override IReadOnlyList\u003CIParam\u003E Parameters =\u003E new IParam[] { Bodies, Levels, AllFaces };\n\n\t/// \u003Csummary\u003EWhat this feature will cost at the current settings, for a UI that warns before\n\t/// rather than after. Levels are exponential and the jump from 4 to 6 is 16x.\u003C/summary\u003E\n\tpublic (int Vertices, int Faces) PredictCost( IEnumerable\u003CBody\u003E bodies )\n\t{\n\t\tvar v = 0;\n\t\tvar f = 0;\n\t\tvar list = bodies as IList\u003CBody\u003E ?? bodies.ToList();\n\n\t\tif ( Faces.Count \u003E 0 )\n\t\t{\n\t\t\t// Local subdivision only touches the bodies that were picked on, so the bodies it did\n\t\t\t// not touch still cost exactly what they already are.\n\t\t\tvar picked = Resolve( list, out _ );\n\n\t\t\tforeach ( var body in list )\n\t\t\t{\n\t\t\t\tvar (bv, bf) = picked.TryGetValue( body, out var indices )\n\t\t\t\t\t? CatmullClark.PredictLocalCost( body.Mesh, indices, Levels.Clamped )\n\t\t\t\t\t: (body.Mesh.VertexCount, body.Mesh.FaceCount);\n\n\t\t\t\tv \u002B= bv;\n\t\t\t\tf \u002B= bf;\n\t\t\t}\n\n\t\t\treturn (v, f);\n\t\t}\n\n\t\tforeach ( var body in list.Where( Bodies.Matches ) )\n\t\t{\n\t\t\tvar (bv, bf) = AllFaces.Value\n\t\t\t\t? CatmullClark.PredictLocalCost( body.Mesh, AllIndices( body.Mesh ), Levels.Clamped )\n\t\t\t\t: CatmullClark.PredictCost( body.Mesh, Levels.Clamped );\n\n\t\t\tv \u002B= bv;\n\t\t\tf \u002B= bf;\n\t\t}\n\n\t\treturn (v, f);\n\t}\n\n\t/// \u003Csummary\u003EPicked faces grouped by the body they landed on. \u003Cparamref name=\u0022lost\u0022/\u003E counts the\n\t/// references that no longer resolve \u2014 geometry upstream changed under them.\u003C/summary\u003E\n\tDictionary\u003CBody, List\u003Cint\u003E\u003E Resolve( IEnumerable\u003CBody\u003E bodies, out int lost )\n\t{\n\t\tvar byBody = new Dictionary\u003CBody, List\u003Cint\u003E\u003E();\n\t\tlost = 0;\n\n\t\tforeach ( var reference in Faces )\n\t\t{\n\t\t\tif ( !FacePlane.TryResolveFace( bodies, reference, out var body, out var faceIndex ) )\n\t\t\t{\n\t\t\t\tlost\u002B\u002B;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ( !byBody.TryGetValue( body, out var indices ) )\n\t\t\t\tbyBody[body] = indices = new List\u003Cint\u003E();\n\n\t\t\tindices.Add( faceIndex );\n\t\t}\n\n\t\treturn byBody;\n\t}\n\n\tprotected override void Execute( FeatureContext ctx )\n\t{\n\t\tif ( Levels.Clamped == 0 )\n\t\t\treturn;\n\n\t\tif ( Faces.Count \u003E 0 )\n\t\t{\n\t\t\t// A picked face already names its body, so the Bodies filter has nothing left to decide and\n\t\t\t// is not consulted here. Two picks on the same body are subdivided in ONE call rather than\n\t\t\t// one call each: the second call would be running against a mesh whose face indices the\n\t\t\t// first has already renumbered.\n\t\t\tvar picked = Resolve( ctx.Bodies, out var lost );\n\n\t\t\tif ( picked.Count == 0 )\n\t\t\t{\n\t\t\t\tFail(\n\t\t\t\t\t\u0022None of the picked faces are still there\u0022,\n\t\t\t\t\t\u0022Every face this feature subdivides was removed or replaced by a change further up the tree.\u0022,\n\t\t\t\t\t\u0022Pick the faces again\u0022,\n\t\t\t\t\t\u0022Or clear the picks to subdivide the whole body\u0022 );\n\t\t\t}\n\n\t\t\tforeach ( var (body, indices) in picked )\n\t\t\t\tbody.Mesh = CatmullClark.SubdivideFaces( body.Mesh, indices, Levels.Clamped );\n\n\t\t\tif ( lost \u003E 0 )\n\t\t\t{\n\t\t\t\tWarn(\n\t\t\t\t\t$\u0022{lost} picked {(lost == 1 ? \u0022face is\u0022 : \u0022faces are\u0022)} no longer there\u0022,\n\t\t\t\t\t\u0022A change further up the tree removed or replaced them, so they were skipped.\u0022,\n\t\t\t\t\t\u0022Pick them again if the extra density is still wanted\u0022 );\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\t// No picks. AllFaces chooses between densifying every face (linear, shape unchanged) and\n\t\t// the whole-body Catmull-Clark that smooths.\n\t\tforeach ( var body in RequireBodies( ctx, Bodies ) )\n\t\t{\n\t\t\tif ( AllFaces.Value )\n\t\t\t\tbody.Mesh = CatmullClark.SubdivideFaces( body.Mesh, AllIndices( body.Mesh ), Levels.Clamped );\n\t\t\telse\n\t\t\t\tbody.Mesh = CatmullClark.Subdivide( body.Mesh, Levels.Clamped );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EEvery face index of a mesh, as a list \u2014 the \u0022all faces\u0022 version of a pick list.\u003C/summary\u003E\n\tstatic int[] AllIndices( PolyMesh mesh )\n\t{\n\t\tvar all = new int[mesh.FaceCount];\n\n\t\tfor ( var i = 0; i \u003C all.Length; i\u002B\u002B )\n\t\t\tall[i] = i;\n\n\t\treturn all;\n\t}\n}\n"},{"Ident":"pooh.geppetto","Path":"Editor/Effigy/Features/Feature.cs","FileName":"Feature.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":411088,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Diagnostics.CodeAnalysis;\nusing System.Linq;\nusing System.Reflection;\n\nnamespace Effigy;\n\n/// \u003Csummary\u003EOne solid in the studio. Onshape calls these parts; a Part Studio holds several.\u003C/summary\u003E\n/// \u003Csummary\u003E\n/// A picture stood in the viewport to model against. \u003Csee cref=\u0022View\u0022/\u003E is which way it faces:\n/// \u0022front\u0022 stands in the XZ plane facing the Front view, \u0022side\u0022 in the YZ plane facing Right,\n/// \u0022top\u0022 lies flat facing Top. \u003Csee cref=\u0022Height\u0022/\u003E is its size in units (width follows the\n/// picture); \u003Csee cref=\u0022Offset\u0022/\u003E slides it in its own plane; \u003Csee cref=\u0022Depth\u0022/\u003E pushes it back\n/// away from the viewer, so it sits behind the model rather than through it.\n/// \u003C/summary\u003E\npublic sealed class ReferenceImage\n{\n\tpublic string Path = \u0022\u0022;\n\tpublic string View = \u0022front\u0022;\n\tpublic float Height = 72f;\n\tpublic Vec2 Offset;\n\tpublic float Depth = 0f;\n\tpublic float Opacity = 0.5f;\n\tpublic bool Visible = true;\n\n\tpublic ReferenceImage Clone() =\u003E new()\n\t{\n\t\tPath = Path, View = View, Height = Height, Offset = Offset, Depth = Depth, Opacity = Opacity, Visible = Visible,\n\t};\n}\n\npublic sealed class Body\n{\n\tpublic string Id;\n\tpublic string Name;\n\tpublic PolyMesh Mesh;\n\n\t/// \u003Csummary\u003EWhether this body is drawn. Set from the feature that produced it \u2014 see\n\t/// Feature.Visible for why this is not the same thing as suppression.\u003C/summary\u003E\n\tpublic bool Visible = true;\n\n\t/// \u003Csummary\u003EId of the Feature that produced this body, recorded by the rebuild alongside\n\t/// Visible. Bodies are re-made from scratch every rebuild and have no identity a UI can hold\n\t/// onto, so a Parts list needs this to get from a row back to the thing that owns it.\u003C/summary\u003E\n\tpublic string FeatureId;\n\n\t/// \u003Csummary\u003EMade by a GarmentFeature. Later garments collide with it but are never cut from it,\n\t/// and Fur with nothing picked grows on garments.\u003C/summary\u003E\n\tpublic bool IsGarment;\n\n\t/// \u003Csummary\u003EHere to be measured against, not to be shipped \u2014 the body a WearerFeature brought\n\t/// in so a garment has something to be cut from and collide with. Everything reads it the way\n\t/// it reads any other body; only export leaves it out. See WearerFeature.\u003C/summary\u003E\n\tpublic bool IsReference;\n\n\tpublic Body( string id, string name, PolyMesh mesh )\n\t{\n\t\tId = id;\n\t\tName = name;\n\t\tMesh = mesh;\n\t}\n\n\tpublic Body Clone() =\u003E new( Id, Name, Mesh.Clone() )\n\t\t{ Visible = Visible, FeatureId = FeatureId, IsGarment = IsGarment, IsReference = IsReference };\n}\n\n// --- parameters -------------------------------------------------------------------------------\n\n/// \u003Csummary\u003E\n/// Feature parameters describe themselves, so one generic panel can render any feature\u0027s dialog\n/// rather than each feature needing hand-written UI.\n///\n/// This is copied from Onshape deliberately. Every feature dialog there has the same shape, and\n/// that uniformity is most of why the tool feels coherent with a hundred features in it. Getting\n/// it for free is worth the small indirection here.\n///\n/// The parameter object IS the storage \u2014 the feature reads \u0060_length.Value\u0060 \u2014 so there is no\n/// separate copy to keep in sync.\n/// \u003C/summary\u003E\npublic interface IParam\n{\n\tstring Label { get; }\n}\n\npublic sealed class FloatParam : IParam\n{\n\tpublic string Label { get; }\n\tpublic float Value;\n\tpublic float Min, Max;\n\tpublic string Unit;\n\n\tpublic FloatParam( string label, float value, float min = float.MinValue, float max = float.MaxValue, string unit = null )\n\t{\n\t\tLabel = label;\n\t\tValue = value;\n\t\tMin = min;\n\t\tMax = max;\n\t\tUnit = unit;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The expression this parameter was typed as, or null when the number is a literal.\n\t///\n\t/// A LITERAL STAYS A LITERAL. Typing \u003Cc\u003E4\u003C/c\u003E must not become a formula that re-evaluates;\n\t/// typing \u003Cc\u003E#thickness / 2\u003C/c\u003E must, or changing the variable does not move the feature.\n\t/// Rebuild evaluates this through the studio\u0027s variable table and writes the result into\n\t/// \u003Csee cref=\u0022Value\u0022/\u003E. An empty or unparsable expression holds the last good value.\n\t/// \u003C/summary\u003E\n\tpublic string Expr;\n\n\tpublic float Clamped =\u003E Math.Clamp( Value, Min, Max );\n}\n\npublic sealed class IntParam : IParam\n{\n\tpublic string Label { get; }\n\tpublic int Value;\n\tpublic int Min, Max;\n\n\t/// \u003Csummary\u003E\n\t/// Whether dragging this number through its range means anything.\n\t///\n\t/// A slider is for a MAGNITUDE \u2014 segment counts, subdivision levels, numbers where the values\n\t/// either side of the one you have are the neighbouring answers. A material slot is an\n\t/// IDENTIFIER that happens to be stored as a number: slot 7 is not \u0022more\u0022 than slot 6, sweeping\n\t/// through 40 of them on the way says nothing, and the drag bar was the widest control in the\n\t/// Extrude dialog doing the least. Those get a field and no slider.\n\t///\n\t/// The bounds test in the dialog cannot tell the two apart \u2014 0..63 looks exactly as draggable\n\t/// as 0..6 \u2014 so the parameter says which it is.\n\t/// \u003C/summary\u003E\n\tpublic bool Slider = true;\n\n\tpublic IntParam( string label, int value, int min = int.MinValue, int max = int.MaxValue )\n\t{\n\t\tLabel = label;\n\t\tValue = value;\n\t\tMin = min;\n\t\tMax = max;\n\t}\n\n\tpublic int Clamped =\u003E Math.Clamp( Value, Min, Max );\n}\n\npublic sealed class BoolParam : IParam\n{\n\tpublic string Label { get; }\n\tpublic bool Value;\n\n\tpublic BoolParam( string label, bool value )\n\t{\n\t\tLabel = label;\n\t\tValue = value;\n\t}\n}\n\npublic sealed class Vec3Param : IParam\n{\n\tpublic string Label { get; }\n\tpublic Vec3 Value;\n\n\tpublic Vec3Param( string label, Vec3 value )\n\t{\n\t\tLabel = label;\n\t\tValue = value;\n\t}\n}\n\npublic sealed class ChoiceParam : IParam\n{\n\tpublic string Label { get; }\n\tpublic string[] Options;\n\tpublic int Index;\n\n\tpublic ChoiceParam( string label, string[] options, int index = 0 )\n\t{\n\t\tLabel = label;\n\t\tOptions = options;\n\t\tIndex = index;\n\t}\n\n\tpublic string Value =\u003E Options[Math.Clamp( Index, 0, Options.Length - 1 )];\n}\n\n/// \u003Csummary\u003E\n/// A free-text parameter. Paths, names, anything a number or a dropdown cannot hold.\n///\n/// Written as the rest of the line, so a path with spaces survives. The dialog renders it as a\n/// field; Import is the feature that asked for it.\n/// \u003C/summary\u003E\npublic sealed class StringParam : IParam\n{\n\tpublic string Label { get; }\n\tpublic string Value;\n\n\tpublic StringParam( string label, string value = \u0022\u0022 )\n\t{\n\t\tLabel = label;\n\t\tValue = value ?? \u0022\u0022;\n\t}\n}\n\n/// \u003Csummary\u003EWhich bodies a feature acts on. Empty means every body, which is what Onshape\u0027s\n/// \u0022all\u0022 behaves like and is the sane default for a studio holding one part.\u003C/summary\u003E\npublic sealed class BodySelectionParam : IParam\n{\n\tpublic string Label { get; }\n\tpublic List\u003Cstring\u003E BodyIds = new();\n\n\tpublic BodySelectionParam( string label )\n\t{\n\t\tLabel = label;\n\t}\n\n\tpublic bool Matches( Body b ) =\u003E BodyIds.Count == 0 || BodyIds.Contains( b.Id );\n}\n\n/// \u003Csummary\u003E\n/// What geometry a tool will take from the selection you already made.\n///\n/// ONE DECLARATION BECAUSE THERE WERE THREE. \u0022Which tools consume a face\u0022 was written down in three\n/// places that could not see each other - the type switch in \u003Csee cref=\u0022Feature.ApplyGeometrySelection\u0022/\u003E,\n/// the prose the editor paints under the viewport, and the per-dialog pick-mode flags - so the strip\n/// could be right while the hint was a lie, and a tool could be missing from a list it belonged on\n/// with nothing to catch it. Nothing DECLARED it, so nothing could ASK.\n///\n/// A FLAGS ENUM ON A VIRTUAL PROPERTY, deliberately, and for the same reason EffigyWindow.ToolKind\n/// is an enum rather than a Func: an int survives a hotload, a lambda or a System.Type held in\n/// static editor state comes back pointing into the dead assembly.\n///\n/// This says what a tool will USE, not what it needs to run. Fillet accepts a body because selecting\n/// a part and pressing Fillet fillets that part; it does not follow that a fillet with no selection\n/// is invalid.\n/// \u003C/summary\u003E\n[Flags]\npublic enum GeometryKind\n{\n\tNone = 0,\n\tFace = 1,\n\tEdge = 2,\n\tBody = 4,\n\tSketchRegion = 8,\n}\n\n// --- features ---------------------------------------------------------------------------------\n\n/// \u003Csummary\u003EThe state a feature reads and writes as it runs.\u003C/summary\u003E\npublic sealed class FeatureContext\n{\n\tpublic List\u003CBody\u003E Bodies = new();\n\n\t/// \u003Csummary\u003ESketches published by SketchFeature, keyed by that feature\u0027s id. Extrude and\n\t/// Revolve look themselves up here rather than holding a reference, so editing the sketch\n\t/// upstream and rebuilding feeds the new geometry through without any wiring to keep in sync.\u003C/summary\u003E\n\tpublic Dictionary\u003Cstring, Sketch\u003E Sketches = new();\n\n\t/// \u003Csummary\u003ESplines published by SplineFeature, keyed by that feature\u0027s id, and the id of the\n\t/// one published last \u2014 what a curve deform or a sweep means by \u0022the spline\u0022 when it names\n\t/// none. Same arrangement as Sketches, for the same reason.\u003C/summary\u003E\n\tpublic Dictionary\u003Cstring, Spline3\u003E Splines = new();\n\tpublic string LastSplineId;\n\n\t/// \u003Csummary\u003E\n\t/// For a sketch drawn on a face, the id of the body that face belongs to. Keyed by sketch\n\t/// feature id, same as Sketches.\n\t///\n\t/// This is what lets an extrude know it is growing OUT OF something rather than into thin air.\n\t/// A Sketch is pure geometry and has no business knowing about bodies, and the consuming\n\t/// feature never sees the SketchFeature itself \u2014 only what it published here \u2014 so the\n\t/// attachment travels the same way the sketch does.\n\t/// \u003C/summary\u003E\n\tpublic Dictionary\u003Cstring, string\u003E SketchHostBodies = new();\n\n\t/// \u003Csummary\u003E\n\t/// Planes published by PlaneFeature, keyed by that feature\u0027s id. A sketch \u2014 or another plane \u2014\n\t/// looks itself up here rather than holding the SketchPlane object.\n\t///\n\t/// Beside Sketches and for exactly the same reason: move the plane, rebuild, and everything\n\t/// drawn on it follows, with no second reference anywhere that could be left pointing at the old\n\t/// one. It is also what makes a reference to a plane that is suppressed or below the rollback\n\t/// bar fail honestly rather than quietly using last rebuild\u0027s answer \u2014 a feature that has not\n\t/// run is not in here.\n\t/// \u003C/summary\u003E\n\tpublic Dictionary\u003Cstring, SketchPlane\u003E Planes = new();\n\n\t/// \u003Csummary\u003E\n\t/// For a plane built from a face, the id of the body that face belongs to \u2014 inherited along a\n\t/// chain of planes. Keyed by plane feature id, same as Planes.\n\t///\n\t/// The plane half of SketchHostBodies above, and it feeds straight into it: a sketch on a plane\n\t/// that came off a part is a sketch on that part as far as Extrude\u0027s Auto result is concerned,\n\t/// which is what keeps \u0022three bosses off one block\u0022 one part rather than four.\n\t/// \u003C/summary\u003E\n\tpublic Dictionary\u003Cstring, string\u003E PlaneHostBodies = new();\n\n\t/// \u003Csummary\u003E\n\t/// Resolve a variable name (without the \u003Cc\u003E#\u003C/c\u003E) during this rebuild. Null when the studio\n\t/// has no table. Set once before the first feature runs, so a feature\u0027s value does not depend\n\t/// on when it happened to be asked.\n\t/// \u003C/summary\u003E\n\tpublic Func\u003Cstring, float?\u003E Resolve;\n\n\t/// \u003Csummary\u003EThe studio\u0027s rig, read-only, for features that place things by bone - a garment\n\t/// finds the torso and the arms from it. Null when the studio has none.\u003C/summary\u003E\n\tpublic Skeleton Rig;\n\n\t/// \u003Csummary\u003E\n\t/// This rebuild is a PREVIEW: somebody is dragging a slider and wants to see the shape move, not\n\t/// to see the finished article.\n\t///\n\t/// A feature that has an expensive final step and a cheap approximation of it should take the\n\t/// cheap one here. A garment\u0027s drape is forty simulation steps and its solidify doubles the\n\t/// mesh; neither changes the silhouette you are actually looking at while you drag Length, and\n\t/// together they are the difference between a slider that moves and one that stutters.\n\t///\n\t/// NOT A QUALITY SETTING. Nothing is saved from a preview rebuild and nothing exports from one -\n\t/// the full rebuild always follows, within a fraction of a second of the drag stopping. A\n\t/// feature must therefore never let a preview change anything but geometry: no material slots\n\t/// moving, no bodies appearing or disappearing, or the tree and the parts list would flicker\n\t/// through states that never existed.\n\t/// \u003C/summary\u003E\n\tpublic bool Preview;\n\n\tint _nextId = 1;\n\tstring _featureId;\n\tint _featureBodies;\n\n\t/// \u003Csummary\u003E\n\t/// A body id belonging to the feature currently running: its feature id, plus a counter of the\n\t/// bodies that feature has made this run.\n\t///\n\t/// IDS USED TO BE A SINGLE RUNNING COUNTER - body1, body2, body3 in creation order across the\n\t/// whole rebuild - and that is the topological naming problem all over again, one level up from\n\t/// the faces FaceRef was written to protect. Add a feature ANYWHERE upstream that produces a\n\t/// body and every id after it shifts by one, so a sketch attached to \u0022body1\u0022 silently lands on\n\t/// whatever happens to be body1 now. Not an error, not a warning: a boss quietly reattaches\n\t/// itself to an unrelated block.\n\t///\n\t/// Feature ids are assigned once at creation and never reused, so a body named after the\n\t/// feature that made it cannot be renumbered by anything happening elsewhere in the tree.\n\t/// \u003C/summary\u003E\n\tpublic string NewBodyId() =\u003E\n\t\t_featureId is null ? $\u0022body{_nextId\u002B\u002B}\u0022 : $\u0022{_featureId}b{_featureBodies\u002B\u002B}\u0022;\n\n\t/// \u003Csummary\u003ECalled by Feature.Run before Execute, so bodies are named after their maker. The\n\t/// per-feature counter restarts, which is what makes a feature that produces three bodies -\n\t/// a pattern, say - name them the same three ids on every rebuild.\u003C/summary\u003E\n\tpublic void BeginFeature( string featureId )\n\t{\n\t\t_featureId = featureId;\n\t\t_featureBodies = 0;\n\t}\n\n\t/// \u003Csummary\u003EVestigial: only the fallback numbering uses it, for a context nothing has called\n\t/// BeginFeature on.\u003C/summary\u003E\n\tpublic void SeedIdCounter( int next ) =\u003E _nextId = Math.Max( _nextId, next );\n}\n\n/// \u003Csummary\u003E\n/// One step in the history. Features run in order, each seeing what the ones before it produced.\n///\n/// THE ORDERED TREE IS THE WHOLE POINT. It is what separates a parametric modeller from a stack of\n/// bakes: roll back to before feature 3, change a number, roll forward and everything downstream\n/// rebuilds against the new value. Every design decision here bends toward keeping that true \u2014\n/// features hold parameters rather than results, and nothing caches geometry inside a feature.\n/// \u003C/summary\u003E\npublic abstract class Feature\n{\n\tpublic string Id = Guid.NewGuid().ToString( \u0022N\u0022 )[..8];\n\tpublic string Name;\n\n\t/// \u003Csummary\u003E\n\t/// Suppressed features do not run at all \u2014 their effect is removed from the model.\n\t/// \u003C/summary\u003E\n\tpublic bool Suppressed;\n\n\t/// \u003Csummary\u003E\n\t/// Whether the geometry this feature produced is DRAWN. Deliberately not the same thing as\n\t/// Suppressed, and conflating the two would be wrong: suppression takes a feature\u0027s effect out\n\t/// of the model, hiding only stops you looking at it. A hidden body is still there, still\n\t/// exported, and everything downstream still builds on it.\n\t///\n\t/// Solvespace keeps the same two flags separately on a group for the same reason.\n\t/// \u003C/summary\u003E\n\tpublic bool Visible = true;\n\n\t/// \u003Csummary\u003ESet by Run when Execute threw. A failed feature does not stop the rebuild \u2014 the\n\t/// tree carries on with the bodies as they were, which is what lets you fix an upstream\n\t/// mistake without every later feature also reporting failure.\u003C/summary\u003E\n\tpublic string Error { get; internal set; }\n\n\t/// \u003Csummary\u003E\n\t/// Something the feature worked around rather than failed on \u2014 it built, but not from\n\t/// everything it was given.\n\t///\n\t/// Distinct from Error because the two need opposite handling. An error means there is no\n\t/// geometry and the tree below is standing on nothing; a warning means there IS geometry and\n\t/// you should look at it. Collapsing the second into the first is why one stray branch\n\t/// anywhere in a sketch used to fail every extrude that read it.\n\t/// \u003C/summary\u003E\n\tpublic string Warning { get; internal set; }\n\n\t/// \u003Csummary\u003E\n\t/// Structured form of Error or Warning. Null when the feature ran clean.\n\t///\n\t/// Error and Warning stay as strings because PartStudio, RebuildReport, the feature tree and\n\t/// several tests already read them. They are set alongside this: Error = Diagnostic.Problem\n\t/// on a failure, Warning = Diagnostic.Problem on a warning.\n\t/// \u003C/summary\u003E\n\tpublic FeatureDiagnostic Diagnostic { get; internal set; }\n\n\tpublic abstract string TypeName { get; }\n\tpublic abstract IReadOnlyList\u003CIParam\u003E Parameters { get; }\n\n\t/// \u003Csummary\u003E\n\t/// The subset of \u003Csee cref=\u0022Parameters\u0022/\u003E that belongs behind the dialog\u0027s Advanced disclosure\n\t/// \u2014 folded away by default, and still every bit as much a parameter as the ones above it.\n\t///\n\t/// It is a SUBSET RATHER THAN A SECOND LIST. Everything generic \u2014 the snapshot Cancel restores\n\t/// from, the document writer, the diagnostics that point at a parameter by label \u2014 walks\n\t/// Parameters, and a parameter that only appeared here would be invisible to all of it. So this\n\t/// says nothing about what a feature HAS; it only says which of them are answered once and then\n\t/// left alone. Extrude is the case that asked for it: taper, a second distance and a material\n\t/// slot are three rows of nothing-to-do-here sitting under the distance that is the whole point\n\t/// of the feature.\n\t///\n\t/// Held by REFERENCE, matched by reference. Naming the parameters by label would tie which rows\n\t/// fold up to the words on screen.\n\t/// \u003C/summary\u003E\n\tpublic virtual IReadOnlyList\u003CIParam\u003E AdvancedParameters =\u003E Array.Empty\u003CIParam\u003E();\n\n\t/// \u003Csummary\u003E\n\t/// Whether this feature\u0027s cached result is out of date even though nobody called MarkDirty.\n\t///\n\t/// The convention everywhere else is that whoever edits a feature marks it dirty, and for a\n\t/// dialog full of numbers that is one call in one place. It does not hold for a feature whose\n\t/// state is a live object somebody else is mutating \u2014 SculptFeature\u0027s levels are changed by a\n\t/// brush, hundreds of times a stroke, nowhere near the code that owns the studio. Relying on\n\t/// that caller to remember is how \u0022every parameter edit is a silent no-op\u0022 happened once\n\t/// already, and it looked like three unrelated UI faults for a day.\n\t///\n\t/// So a feature that owns mutable state outside its parameters answers this instead, and the\n\t/// rebuild asks. Must be cheap: it is called for every reusable feature on every rebuild.\n\t/// \u003C/summary\u003E\n\tpublic virtual bool IsStale =\u003E false;\n\n\t/// \u003Csummary\u003E\n\t/// Which kinds of picked geometry this feature will actually use.\n\t///\n\t/// The one place that knows. \u003Csee cref=\u0022ApplyGeometrySelection\u0022/\u003E is what DOES the consuming, and\n\t/// the two are kept honest against each other by AcceptsTests rather than by hope: a feature that\n\t/// stores FaceRefs and forgets to say Face here fails a test, not a user.\n\t///\n\t/// Must be cheap and must not touch the studio - the editor asks this while painting the toolbar.\n\t/// \u003C/summary\u003E\n\tpublic virtual GeometryKind Accepts =\u003E GeometryKind.None;\n\n\t/// \u003Csummary\u003E\n\t/// Seed this feature from geometry that was already picked, the way Onshape\u0027s tools consume\n\t/// the current selection instead of making you pick again after the button.\n\t///\n\t/// Faces go to anything that stores FaceRefs \u2014 a sketch\u0027s plane, a datum plane, draft, hole,\n\t/// face material, subdivide, move face, and EXTRUDE, which pulls the face itself. Body ids go onto every\n\t/// BodySelectionParam. A face selection with no explicit\n\t/// body list still names those faces\u0027 bodies, so clicking a face and then Fillet fillets that\n\t/// part rather than every part.\n\t///\n\t/// \u003Cparamref name=\u0022bodies\u0022/\u003E is only needed for Shell, whose openings are still stored as\n\t/// indices rather than FaceRefs and have to be resolved against the live mesh.\n\t/// \u003C/summary\u003E\n\tpublic void ApplyGeometrySelection( IReadOnlyList\u003CFaceRef\u003E faces, IReadOnlyList\u003Cstring\u003E bodyIds,\n\t\tIEnumerable\u003CBody\u003E bodies = null, IReadOnlyList\u003CEdgeRef\u003E edges = null,\n\t\tstring sketchFeatureId = null, IReadOnlyList\u003CVec2\u003E regionSeeds = null )\n\t{\n\t\tfaces ??= Array.Empty\u003CFaceRef\u003E();\n\t\tbodyIds ??= Array.Empty\u003Cstring\u003E();\n\t\tedges ??= Array.Empty\u003CEdgeRef\u003E();\n\n\t\tif ( this is SketchFeature sketch \u0026\u0026 faces.Count \u003E 0 )\n\t\t{\n\t\t\tsketch.Face = faces[0];\n\t\t\tsketch.PlaneFeatureId = \u0022\u0022;\n\t\t}\n\n\t\t// A datum plane takes a face the same way a sketch does, and the two other things it could\n\t\t// have been built from are cleared: they are three answers to one question, and a stale one\n\t\t// left set would win on the next rebuild (see PlaneFeature.ResolveBasePlane\u0027s order).\n\t\tif ( this is PlaneFeature datum \u0026\u0026 faces.Count \u003E 0 )\n\t\t{\n\t\t\tdatum.Face = faces[0];\n\t\t\tdatum.BasePlaneId = \u0022\u0022;\n\t\t}\n\n\t\tif ( this is SketchConsumingFeature consumer\n\t\t\t\u0026\u0026 !string.IsNullOrEmpty( sketchFeatureId )\n\t\t\t\u0026\u0026 sketchFeatureId != SketchConsumingFeature.AwaitingPick )\n\t\t{\n\t\t\tconsumer.SketchFeatureId = sketchFeatureId;\n\t\t\tconsumer.RegionSeeds.Clear();\n\n\t\t\tif ( regionSeeds is { Count: \u003E 0 } )\n\t\t\t\tconsumer.RegionSeeds.AddRange( regionSeeds );\n\t\t}\n\n\t\tvar pickedFaces = this switch\n\t\t{\n\t\t\tFaceMaterialFeature material =\u003E material.Faces,\n\t\t\tDraftFeature draft =\u003E draft.Faces,\n\t\t\tHoleFeature hole =\u003E hole.Faces,\n\t\t\tSubdivideFeature subdivide =\u003E subdivide.Faces,\n\t\t\tMoveFaceFeature move =\u003E move.Faces,\n\t\t\tExtrudeFeature extrude =\u003E extrude.Faces,\n\t\t\tFurFeature fur =\u003E fur.Faces,\n\t\t\t_ =\u003E null,\n\t\t};\n\n\t\tif ( pickedFaces is not null \u0026\u0026 faces.Count \u003E 0 )\n\t\t{\n\t\t\tpickedFaces.Clear();\n\t\t\tpickedFaces.AddRange( faces );\n\t\t}\n\n\t\t// A FACE ANSWERS THE QUESTION AwaitingPick WAS ASKING. An Extrude created from the toolbar\n\t\t// starts out refusing to help itself to the nearest sketch (see SketchConsumingFeature\n\t\t// .AwaitingPick), and pointing it at a face is an answer just as much as pointing it at a\n\t\t// sketch is. Leaving the flag set would have the feature build correctly from the face while\n\t\t// every panel that reads it still said \u0022pick a profile\u0022.\n\t\tif ( this is SketchConsumingFeature awaiting \u0026\u0026 faces.Count \u003E 0\n\t\t\t\u0026\u0026 pickedFaces is not null \u0026\u0026 awaiting.IsAwaitingPick )\n\t\t\tawaiting.SketchFeatureId = \u0022\u0022;\n\n\t\tApplyBlendEdges( faces, edges, bodies );\n\n\t\tvar ids = bodyIds.Count \u003E 0\n\t\t\t? bodyIds\n\t\t\t: (IReadOnlyList\u003Cstring\u003E)faces.Select( f =\u003E f.BodyId )\n\t\t\t\t.Concat( edges.Select( e =\u003E e.BodyId ) )\n\t\t\t\t.Distinct()\n\t\t\t\t.ToList();\n\n\t\tif ( ids.Count \u003E 0 )\n\t\t{\n\t\t\tforeach ( var param in Parameters )\n\t\t\t{\n\t\t\t\tif ( param is not BodySelectionParam selection )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tselection.BodyIds.Clear();\n\t\t\t\tselection.BodyIds.AddRange( ids );\n\t\t\t}\n\t\t}\n\n\t\tif ( this is not ShellFeature shell || faces.Count == 0 || bodies is null )\n\t\t\treturn;\n\n\t\t// OpenFaces is applied to every selected body, so it only means something when the\n\t\t// openings all live on one part. Two parts and a mixed index list would punch the\n\t\t// wrong faces \u2014 or fail \u2014 on the second body.\n\t\tif ( ids.Count != 1 )\n\t\t\treturn;\n\n\t\tshell.OpenFaces.Clear();\n\n\t\tforeach ( var face in faces )\n\t\t{\n\t\t\tif ( !FacePlane.TryResolveFace( bodies, face, out _, out var index ) )\n\t\t\t\tcontinue;\n\n\t\t\tif ( !shell.OpenFaces.Contains( index ) )\n\t\t\t\tshell.OpenFaces.Add( index );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Fillet and Chamfer store edges, not faces. A picked edge list is copied across; a picked\n\t/// FACE becomes that face\u0027s boundary, which is what \u0022select the top, then Fillet\u0022 means in\n\t/// Onshape.\n\t/// \u003C/summary\u003E\n\tvoid ApplyBlendEdges( IReadOnlyList\u003CFaceRef\u003E faces, IReadOnlyList\u003CEdgeRef\u003E edges, IEnumerable\u003CBody\u003E bodies )\n\t{\n\t\tvar dest = this switch\n\t\t{\n\t\t\tFilletFeature fillet =\u003E fillet.Edges,\n\t\t\tChamferFeature chamfer =\u003E chamfer.Edges,\n\t\t\t_ =\u003E null,\n\t\t};\n\n\t\tif ( dest is null )\n\t\t\treturn;\n\n\t\tdest.Clear();\n\n\t\tif ( edges.Count \u003E 0 )\n\t\t{\n\t\t\tdest.AddRange( edges );\n\t\t\treturn;\n\t\t}\n\n\t\tif ( faces.Count == 0 || bodies is null )\n\t\t\treturn;\n\n\t\tvar seen = new HashSet\u003C(string BodyId, EdgeKey Key)\u003E();\n\n\t\tforeach ( var face in faces )\n\t\t{\n\t\t\tif ( !FacePlane.TryResolveFace( bodies, face, out var body, out var index ) )\n\t\t\t\tcontinue;\n\n\t\t\tforeach ( var edge in FacePlane.CaptureBoundary( body, index ) )\n\t\t\t{\n\t\t\t\tif ( !FacePlane.TryResolveEdge( bodies, edge, out _, out var key ) )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tif ( !seen.Add( (body.Id, key) ) )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tdest.Add( edge );\n\t\t\t}\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Re-evaluate every \u003Csee cref=\u0022FloatParam\u0022/\u003E that still carries its typed expression, so a\n\t/// variable change moves the feature without anyone reopening the dialog.\n\t///\n\t/// ALL PUBLIC FLOAT FIELDS, not only \u003Csee cref=\u0022Parameters\u0022/\u003E. PrimitiveFeature hides SizeX\n\t/// when the shape is a cylinder, and Execute still reads it; skipping hidden ones would leave\n\t/// a stale number the moment the dropdown flipped back.\n\t/// \u003C/summary\u003E\n\tinternal void EvaluateExpressions( Func\u003Cstring, float?\u003E resolve )\n\t{\n\t\tforeach ( var field in GetType().GetFields() )\n\t\t{\n\t\t\tif ( field.GetValue( this ) is not FloatParam param )\n\t\t\t\tcontinue;\n\n\t\t\tif ( string.IsNullOrWhiteSpace( param.Expr ) )\n\t\t\t\tcontinue;\n\n\t\t\tif ( !Expression.TryEvaluate( param.Expr, param.Unit, resolve, out var value ) )\n\t\t\t\tcontinue;\n\n\t\t\tparam.Value = Math.Clamp( value, param.Min, param.Max );\n\t\t}\n\t}\n\n\tprotected abstract void Execute( FeatureContext ctx );\n\n\tinternal void Run( FeatureContext ctx )\n\t{\n\t\tError = null;\n\t\tWarning = null;\n\t\tDiagnostic = null;\n\n\t\t// Bodies made from here on belong to this feature and are named after it. Set before the\n\t\t// Suppressed check is pointless; set here so every path into Execute is covered.\n\t\tctx.BeginFeature( Id );\n\n\t\tif ( Suppressed )\n\t\t\treturn;\n\n\t\ttry\n\t\t{\n\t\t\tEvaluateExpressions( ctx.Resolve );\n\t\t\tExecute( ctx );\n\t\t}\n\t\tcatch ( FeatureException e )\n\t\t{\n\t\t\tApplyDiagnostic( e.Diagnostic );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tApplyDiagnostic( new FeatureDiagnostic( DiagnosticSeverity.Error, e.Message ) );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003ERefuse to proceed. The three arguments are the three things the dialog shows.\u003C/summary\u003E\n\t[DoesNotReturn]\n\tprotected static void Fail( string problem, string cause, params string[] remedies ) =\u003E\n\t\tthrow new FeatureException( new FeatureDiagnostic( DiagnosticSeverity.Error, problem, cause, remedies: remedies ) );\n\n\t/// \u003Csummary\u003ERefuse, and name the control the dialog should highlight.\u003C/summary\u003E\n\t[DoesNotReturn]\n\tprotected static void FailOn( string parameterLabel, string problem, string cause, params string[] remedies ) =\u003E\n\t\tthrow new FeatureException( new FeatureDiagnostic( DiagnosticSeverity.Error, problem, cause, parameterLabel, remedies: remedies ) );\n\n\t/// \u003Csummary\u003ERefuse, highlight the control, and offer a button that writes \u003Cparamref name=\u0022suggested\u0022/\u003E\n\t/// into it.\u003C/summary\u003E\n\t[DoesNotReturn]\n\tprotected static void FailOn( string parameterLabel, float suggested, string problem, string cause, params string[] remedies ) =\u003E\n\t\tthrow new FeatureException( new FeatureDiagnostic( DiagnosticSeverity.Error, problem, cause, parameterLabel, suggested, remedies ) );\n\n\t/// \u003Csummary\u003EThe feature built, but not from what was asked. Geometry stays; the dialog goes yellow.\u003C/summary\u003E\n\tprotected void Warn( string problem, string cause, params string[] remedies ) =\u003E\n\t\tApplyDiagnostic( new FeatureDiagnostic( DiagnosticSeverity.Warning, problem, cause, remedies: remedies ) );\n\n\t/// \u003Csummary\u003E\n\t/// After a cut, put every piece the cut severed into the part list. Returns how many bodies\n\t/// were added, so the caller can say so.\n\t///\n\t/// A boolean is allowed to sever a part and has no obligation to mention it \u2014 \u0022one mesh\u0022 is a\n\t/// fine answer to \u0022subtract this\u0022. Whether that mesh is one SOLID is the part list\u0027s question,\n\t/// and this is where it gets asked. See MeshSplit for why connectivity is by shared vertex and\n\t/// why the order the pieces come back in is a promise rather than an implementation detail.\n\t///\n\t/// THE ORIGINAL BODY KEEPS ITS ID, and keeps the largest piece. Everything built on this part is\n\t/// holding that id \u2014 a sketch drawn on one of its faces, a later feature\u0027s body selection \u2014 and\n\t/// a cut must not invalidate them. The offcuts are new bodies named after the feature that made\n\t/// them, which is the same rule every other body in the studio follows.\n\t/// \u003C/summary\u003E\n\tprotected int SeparatePieces( FeatureContext ctx, Body body )\n\t{\n\t\tif ( body?.Mesh is null || MeshSplit.PieceCount( body.Mesh ) \u003C 2 )\n\t\t\treturn 0;\n\n\t\tvar pieces = MeshSplit.ConnectedPieces( body.Mesh );\n\n\t\tbody.Mesh = pieces[0];\n\n\t\t// Straight after the body they came off, so the parts list reads in the order someone would\n\t\t// expect rather than collecting offcuts at the bottom. IndexOf can legitimately miss - a\n\t\t// feature is allowed to hand us a body it has not published - and appending is right then.\n\t\tvar at = ctx.Bodies.IndexOf( body );\n\n\t\tfor ( var i = 1; i \u003C pieces.Count; i\u002B\u002B )\n\t\t{\n\t\t\tvar piece = new Body( ctx.NewBodyId(), $\u0022{body.Name} ({i \u002B 1})\u0022, pieces[i] )\n\t\t\t{\n\t\t\t\tVisible = body.Visible,\n\t\t\t\tFeatureId = Id,\n\t\t\t};\n\n\t\t\tif ( at \u003C 0 )\n\t\t\t\tctx.Bodies.Add( piece );\n\t\t\telse\n\t\t\t\tctx.Bodies.Insert( at \u002B i, piece );\n\t\t}\n\n\t\treturn pieces.Count - 1;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The warning a severing cut earns. Never an error: the geometry is right, and the part list\n\t/// having grown is something to look at rather than something to fix.\n\t/// \u003C/summary\u003E\n\tprotected void WarnSeparated( int added, string bodyName )\n\t{\n\t\tif ( added \u003C= 0 )\n\t\t\treturn;\n\n\t\tWarn(\n\t\t\tadded == 1\n\t\t\t\t? $\u0022This cut separated \u0027{bodyName}\u0027 into two parts\u0022\n\t\t\t\t: $\u0022This cut separated \u0027{bodyName}\u0027 into {added \u002B 1} parts\u0022,\n\t\t\t$\u0022The tool went all the way through, so what was one solid is now {added \u002B 1} that touch nowhere. \u0022\n\t\t\t\t\u002B \u0022The largest keeps the original part\u0027s name and id; the rest are new parts below it.\u0022,\n\t\t\t\u0022Reduce the depth if the cut was meant to be a pocket\u0022,\n\t\t\t\u0022Nothing to fix if separating the part was the intent\u0022 );\n\t}\n\n\t/// \u003Csummary\u003ENames in a sentence: \u0022\u0027A\u0027\u0022, \u0022\u0027A\u0027 and \u0027B\u0027\u0022, \u0022\u0027A\u0027, \u0027B\u0027 and \u0027C\u0027\u0022.\u003C/summary\u003E\n\tprotected static string Listed( IReadOnlyList\u003Cstring\u003E names )\n\t{\n\t\tif ( names is null || names.Count == 0 )\n\t\t\treturn \u0022nothing\u0022;\n\n\t\tvar quoted = new List\u003Cstring\u003E( names.Count );\n\n\t\tforeach ( var name in names )\n\t\t\tquoted.Add( $\u0022\u0027{name}\u0027\u0022 );\n\n\t\tif ( quoted.Count == 1 )\n\t\t\treturn quoted[0];\n\n\t\treturn string.Join( \u0022, \u0022, quoted.GetRange( 0, quoted.Count - 1 ) ) \u002B \u0022 and \u0022 \u002B quoted[^1];\n\t}\n\n\tprotected void ApplyDiagnostic( FeatureDiagnostic diagnostic )\n\t{\n\t\tif ( diagnostic is null )\n\t\t\treturn;\n\n\t\tDiagnostic = diagnostic;\n\n\t\tif ( diagnostic.Severity == DiagnosticSeverity.Error )\n\t\t\tError = diagnostic.Problem;\n\t\telse\n\t\t\tWarning = diagnostic.Problem;\n\t}\n\n\t/// \u003Csummary\u003EThe bodies this feature acts on, or a refusal if there are none. A feature that\n\t/// did nothing is never a success.\u003C/summary\u003E\n\tprotected List\u003CBody\u003E RequireBodies( FeatureContext ctx, BodySelectionParam selection )\n\t{\n\t\tvar bodies = new List\u003CBody\u003E();\n\n\t\tforeach ( var b in ctx.Bodies )\n\t\t{\n\t\t\tif ( selection.Matches( b ) )\n\t\t\t\tbodies.Add( b );\n\t\t}\n\n\t\tif ( bodies.Count \u003E 0 )\n\t\t\treturn bodies;\n\n\t\tif ( ctx.Bodies.Count == 0 )\n\t\t{\n\t\t\tFail(\n\t\t\t\t\u0022This studio has no bodies yet\u0022,\n\t\t\t\t\u0022There is nothing to act on \u2014 the feature list has not produced a solid.\u0022,\n\t\t\t\t\u0022Add a Primitive, or extrude a sketch first\u0022 );\n\t\t}\n\n\t\tFail(\n\t\t\t\u0022No matching body is selected\u0022,\n\t\t\t$\u0022The studio has {ctx.Bodies.Count} body/bodies but none match this feature\u0027s selection.\u0022,\n\t\t\t\u0022Clear the body selection to act on every body\u0022,\n\t\t\t\u0022Pick a body that is still in the studio\u0022 );\n\n\t\treturn bodies;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Run a blend on each selected body, using picked edges when any were given.\n\t///\n\t/// \u003Cparamref name=\u0022edges\u0022/\u003E empty means the blend\u0027s own angle threshold, which is how Fillet\n\t/// and Chamfer behaved before edges could be picked. Non-empty means only those edges, and a\n\t/// body that none of them land on is left alone rather than blended by the threshold \u2014 mixing\n\t/// the two would fillet a part you never pointed at.\n\t/// \u003C/summary\u003E\n\tprotected List\u003C(Body Body, BlendReport Report)\u003E BlendBodies( FeatureContext ctx, List\u003CEdgeRef\u003E edges,\n\t\tFunc\u003CPolyMesh, HashSet\u003CEdgeKey\u003E, BlendReport\u003E blend )\n\t{\n\t\tvar reports = new List\u003C(Body Body, BlendReport Report)\u003E();\n\t\tvar picked = edges is { Count: \u003E 0 };\n\n\t\tforeach ( var body in RequireBodies( ctx, BodiesOf() ) )\n\t\t{\n\t\t\tif ( !picked )\n\t\t\t{\n\t\t\t\treports.Add( (body, blend( body.Mesh, null )) );\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tvar keys = FacePlane.ResolveEdges( body, edges );\n\n\t\t\tif ( keys.Count == 0 )\n\t\t\t\tcontinue;\n\n\t\t\treports.Add( (body, blend( body.Mesh, keys )) );\n\t\t}\n\n\t\tif ( picked \u0026\u0026 reports.Count == 0 )\n\t\t{\n\t\t\tFail(\n\t\t\t\t\u0022None of the picked edges are on the selected bodies\u0022,\n\t\t\t\t\u0022The edges were stored as geometry so they could survive a rebuild, and none of them could be re-found.\u0022,\n\t\t\t\t\u0022Pick the edges again\u0022,\n\t\t\t\t\u0022Clear the edge selection to blend every sharp edge\u0022 );\n\t\t}\n\n\t\treturn reports;\n\t}\n\n\t/// \u003Csummary\u003EThe BodySelectionParam on this feature, or a fresh empty one (meaning every body)\n\t/// if the feature does not have one. Fillet and Chamfer both do.\u003C/summary\u003E\n\tBodySelectionParam BodiesOf()\n\t{\n\t\tforeach ( var param in Parameters )\n\t\t{\n\t\t\tif ( param is BodySelectionParam selection )\n\t\t\t\treturn selection;\n\t\t}\n\n\t\treturn new BodySelectionParam( \u0022Bodies\u0022 );\n\t}\n\n\t/// \u003Csummary\u003EAssign blended meshes, or refuse, only after every body has been tried \u2014 so a\n\t/// failure leaves the studio as it was, which is Feature.Run\u0027s contract.\u003C/summary\u003E\n\tprotected void CommitBlend( List\u003C(Body Body, BlendReport Report)\u003E reports, string sizeLabel )\n\t{\n\t\tforeach ( var ( _, report ) in reports )\n\t\t{\n\t\t\tif ( report.Failure is null )\n\t\t\t\tcontinue;\n\n\t\t\treport.Failure.ParameterLabel ??= sizeLabel;\n\n\t\t\tif ( report.SuggestedSize \u003E 0f )\n\t\t\t\treport.Failure.SuggestedValue ??= FloorThousandths( report.SuggestedSize );\n\n\t\t\tthrow new FeatureException( report.Failure );\n\t\t}\n\n\t\tforeach ( var ( body, report ) in reports )\n\t\t\tbody.Mesh = report.Mesh;\n\n\t\tvar warnings = new List\u003CFeatureDiagnostic\u003E();\n\n\t\tforeach ( var ( _, report ) in reports )\n\t\t\twarnings.AddRange( report.Warnings );\n\n\t\tif ( warnings.Count == 0 )\n\t\t\treturn;\n\n\t\tif ( warnings.Count == 1 )\n\t\t{\n\t\t\tApplyDiagnostic( warnings[0] );\n\t\t\treturn;\n\t\t}\n\n\t\tWarn(\n\t\t\twarnings[0].Problem,\n\t\t\tstring.Join( \u0022 \u0022, warnings.Select( w =\u003E w.Cause ).Where( c =\u003E !string.IsNullOrEmpty( c ) ) ),\n\t\t\twarnings.SelectMany( w =\u003E w.Remedies ).Distinct().ToArray() );\n\t}\n\n\t/// \u003Csummary\u003EA number the user can type that is never larger than the true fit, so suggesting\n\t/// it cannot immediately fail again to rounding.\u003C/summary\u003E\n\tprotected static float FloorThousandths( float value ) =\u003E MathF.Floor( value * 1000f ) / 1000f;\n\n\tpublic override string ToString() =\u003E $\u0022{TypeName} \u0027{Name ?? Id}\u0027{(Suppressed ? \u0022 (suppressed)\u0022 : \u0022\u0022)}\u0022;\n}\n"},{"Ident":"pooh.geppetto","Path":"Editor/Effigy/GarmentFit.cs","FileName":"GarmentFit.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":411088,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Text;\r\n\r\nnamespace Effigy;\r\n\r\n/// \u003Csummary\u003E\r\n/// Which part of a body a face belongs to, read from the rig rather than from the shape.\r\n///\r\n/// A GARMENT IS A RECIPE, NOT A MESH. \u0022Torso and the top of the upper arms\u0022 means the same thing on\r\n/// the Citizen, on Camhead and on Gearhead, and the rig is the one thing all three have in common.\r\n/// The shape comes from the body itself - see \u003Csee cref=\u0022GarmentFit\u0022/\u003E.\r\n/// \u003C/summary\u003E\r\npublic enum BodyRegion\r\n{\r\n\tNone,\r\n\tHead,\r\n\tNeck,\r\n\tTorso,\r\n\tHips,\r\n\tUpperArm,\r\n\tLowerArm,\r\n\tHand,\r\n\tUpperLeg,\r\n\tLowerLeg,\r\n\tFoot,\r\n}\r\n\r\n/// \u003Csummary\u003EA garment\u0027s coverage: which regions, and how far along each region\u0027s bone.\u003C/summary\u003E\r\npublic sealed class GarmentRecipe\r\n{\r\n\tpublic string Name;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Region, the span of the bone it covers (0 at the bone\u0027s head, 1 at its tail), and which side\r\n\t/// of the body it is on: 0 both, \u002B1 left, -1 right.\r\n\t///\r\n\t/// THE SIDE IS WHAT MAKES REAL CLOTHES POSSIBLE. Everything a span system builds is symmetric by\r\n\t/// construction, and a great deal of clothing is not - one sleeve, a strap over one shoulder, a\r\n\t/// shirt hanging off one side. Tagging the span rather than the recipe means asymmetry composes:\r\n\t/// a garment can be one-sleeved and off-shoulder at once, and each span still means what it\r\n\t/// meant before for anything that does not use it.\r\n\t/// \u003C/summary\u003E\r\n\tpublic List\u003C(BodyRegion Region, float From, float To, int Side)\u003E Spans = new();\r\n\r\n\tpublic static readonly string[] Names =\r\n\t{\r\n\t\t\u0022T-shirt\u0022, \u0022Long sleeve\u0022, \u0022Tank top\u0022, \u0022Trousers\u0022, \u0022Shorts\u0022, \u0022Beanie\u0022, \u0022Gloves\u0022, \u0022Socks\u0022, \u0022Bodysuit\u0022,\r\n\t};\r\n\r\n\t/// \u003Csummary\u003EWhich sleeves or legs a recipe keeps. Sides are the body\u0027s own - left is \u002By, the\r\n\t/// way Source measures it - so \u0022Left only\u0022 is the wearer\u0027s left, not the viewer\u0027s.\u003C/summary\u003E\r\n\tpublic static readonly string[] SideNames = { \u0022Both\u0022, \u0022Left only\u0022, \u0022Right only\u0022 };\r\n\r\n\t/// \u003Csummary\u003ESideNames index to the span tag: both, left (\u002By), right (-y).\u003C/summary\u003E\r\n\tpublic static int SideOf( int index ) =\u003E index switch { 1 =\u003E 1, 2 =\u003E -1, _ =\u003E 0 };\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The recipe with its two lengths applied.\r\n\t///\r\n\t/// \u003Cparamref name=\u0022length\u0022/\u003E is the hem: how far a top reaches down over the hips, how far a leg\r\n\t/// reaches down the leg, how deep a hat sits. \u003Cparamref name=\u0022sleeve\u0022/\u003E is how far down the arm a\r\n\t/// sleeve goes, 0 bare to 1 at the wrist. Both are fractions so they mean the same thing on a\r\n\t/// short body and a tall one.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022neckline\u0022\u003EHow far down the torso the garment STARTS, 0 at the neck. This is the\r\n\t/// top edge, the counterpart of \u003Cparamref name=\u0022length\u0022/\u003E: raise it and the collar becomes a\r\n\t/// scoop, then bares the shoulders, then becomes a tube top. The sleeve starts at the same drop,\r\n\t/// because a sleeve still attached to a shoulder the body of the shirt has left behind is a\r\n\t/// floating ring, not a garment.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022sleeveSide\u0022\u003EWhich sleeve to keep: 0 both, \u002B1 left, -1 right.\u003C/param\u003E\r\n\tpublic static GarmentRecipe Build( int index, float length, float sleeve, float neckline = 0f,\r\n\t\tint sleeveSide = 0 )\r\n\t{\r\n\t\tlength = Math.Clamp( length, 0f, 1f );\r\n\t\tsleeve = Math.Clamp( sleeve, 0f, 1f );\r\n\r\n\t\t// Capped well short of 1: a neckline at the hem is a garment with no garment left, and the\r\n\t\t// fit would refuse with \u0022covers nothing\u0022 rather than explain itself.\r\n\t\tneckline = Math.Clamp( neckline, 0f, 0.8f );\r\n\r\n\t\tvar r = new GarmentRecipe { Name = Names[Math.Clamp( index, 0, Names.Length - 1 )] };\r\n\r\n\t\tvoid Arms( float s )\r\n\t\t{\r\n\t\t\tif ( s \u003C= 0f )\r\n\t\t\t\treturn;\r\n\r\n\t\t\t// The shoulder drop, shared with the torso. Halved because the upper arm\u0027s span runs\r\n\t\t\t// shoulder to elbow and the torso\u0027s runs neck to waist - the same fraction would drop\r\n\t\t\t// the sleeve twice as far down the arm as the neckline dropped down the chest.\r\n\t\t\tvar from = Math.Min( 0.6f, neckline * 0.5f );\r\n\r\n\t\t\tr.Spans.Add( (BodyRegion.UpperArm, from, Math.Max( from, Math.Min( 1f, s * 2f ) ), sleeveSide) );\r\n\r\n\t\t\tif ( s \u003E 0.5f )\r\n\t\t\t\tr.Spans.Add( (BodyRegion.LowerArm, 0f, (s - 0.5f) * 2f, sleeveSide) );\r\n\t\t}\r\n\r\n\t\tvoid Legs( float s )\r\n\t\t{\r\n\t\t\tr.Spans.Add( (BodyRegion.UpperLeg, 0f, Math.Min( 1f, Math.Max( 0.05f, s ) * 2f ), 0) );\r\n\r\n\t\t\tif ( s \u003E 0.5f )\r\n\t\t\t\tr.Spans.Add( (BodyRegion.LowerLeg, 0f, (s - 0.5f) * 2f, 0) );\r\n\t\t}\r\n\r\n\t\tswitch ( r.Name )\r\n\t\t{\r\n\t\t\tcase \u0022T-shirt\u0022:\r\n\t\t\tcase \u0022Long sleeve\u0022:\r\n\t\t\tcase \u0022Tank top\u0022:\r\n\t\t\t\tr.Spans.Add( (BodyRegion.Torso, neckline, 1f, 0) );\r\n\t\t\t\tr.Spans.Add( (BodyRegion.Hips, 0f, length, 0) );\r\n\t\t\t\tArms( r.Name == \u0022Tank top\u0022 ? 0f : sleeve );\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase \u0022Trousers\u0022:\r\n\t\t\tcase \u0022Shorts\u0022:\r\n\t\t\t\tr.Spans.Add( (BodyRegion.Hips, 0f, 1f, 0) );\r\n\t\t\t\tLegs( length );\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase \u0022Beanie\u0022:\r\n\t\t\t\tr.Spans.Add( (BodyRegion.Head, 1f - Math.Max( 0.1f, length ), 1.01f, 0) );\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase \u0022Gloves\u0022:\r\n\t\t\t\tr.Spans.Add( (BodyRegion.Hand, 0f, 1.01f, sleeveSide) );\r\n\t\t\t\tr.Spans.Add( (BodyRegion.LowerArm, 1f - length * 0.5f, 1.01f, sleeveSide) );\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase \u0022Socks\u0022:\r\n\t\t\t\tr.Spans.Add( (BodyRegion.Foot, 0f, 1.01f, 0) );\r\n\t\t\t\tr.Spans.Add( (BodyRegion.LowerLeg, 1f - length * 0.6f, 1.01f, 0) );\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase \u0022Bodysuit\u0022:\r\n\t\t\t\tr.Spans.Add( (BodyRegion.Torso, neckline, 1f, 0) );\r\n\t\t\t\tr.Spans.Add( (BodyRegion.Hips, 0f, 1f, 0) );\r\n\t\t\t\tArms( sleeve );\r\n\t\t\t\tLegs( length );\r\n\t\t\t\tbreak;\r\n\t\t}\r\n\r\n\t\treturn r;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe defaults a recipe opens with: (length, sleeve).\u003C/summary\u003E\r\n\tpublic static (float Length, float Sleeve) Defaults( int index ) =\u003E Names[Math.Clamp( index, 0, Names.Length - 1 )] switch\r\n\t{\r\n\t\t\u0022T-shirt\u0022 =\u003E (0.6f, 0.3f),\r\n\t\t\u0022Long sleeve\u0022 =\u003E (0.7f, 1f),\r\n\t\t\u0022Tank top\u0022 =\u003E (0.6f, 0f),\r\n\t\t\u0022Trousers\u0022 =\u003E (1f, 0f),\r\n\t\t\u0022Shorts\u0022 =\u003E (0.4f, 0f),\r\n\t\t\u0022Beanie\u0022 =\u003E (0.55f, 0f),\r\n\t\t\u0022Gloves\u0022 =\u003E (0.3f, 0f),\r\n\t\t\u0022Socks\u0022 =\u003E (0.4f, 0f),\r\n\t\t_ =\u003E (1f, 1f),\r\n\t};\r\n\r\n\t/// \u003Cparam name=\u0022side\u0022\u003EWhich side of the body the face is on: \u002B1 left, -1 right. A span tagged 0\r\n\t/// covers both, which is what every symmetric garment is made of.\u003C/param\u003E\r\n\tpublic bool Covers( BodyRegion region, float t, int side = 0 ) =\u003E\r\n\t\tSpans.Any( s =\u003E s.Region == region \u0026\u0026 t \u003E= s.From - 1e-4f \u0026\u0026 t \u003C= s.To \u002B 1e-4f\r\n\t\t\t\u0026\u0026 (s.Side == 0 || s.Side == side) );\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Classify a skeleton\u0027s bones into regions by name. Covers Citizen\u0027s names (\u003Cc\u003Earm_upper_R\u003C/c\u003E,\r\n/// \u003Cc\u003Espine_2\u003C/c\u003E), Unreal/Mixamo style (\u003Cc\u003Eupperarm_r\u003C/c\u003E, \u003Cc\u003Ethigh_l\u003C/c\u003E, \u003Cc\u003Emixamorig:LeftForeArm\u003C/c\u003E)\r\n/// and plain English ones people type into the rig panel.\r\n/// \u003C/summary\u003E\r\npublic static class BodyRegions\r\n{\r\n\tpublic static BodyRegion Classify( string boneName )\r\n\t{\r\n\t\tvar n = (boneName ?? \u0022\u0022).ToLowerInvariant();\r\n\r\n\t\tif ( n.Contains( \u0022twist\u0022 ) )\r\n\t\t\tn = n.Replace( \u0022twist\u0022, \u0022\u0022 );\r\n\r\n\t\tbool Any( params string[] keys ) =\u003E keys.Any( n.Contains );\r\n\r\n\t\tif ( Any( \u0022finger\u0022, \u0022thumb\u0022, \u0022index\u0022, \u0022middle\u0022, \u0022ring_\u0022, \u0022pinky\u0022, \u0022hand\u0022 ) ) return BodyRegion.Hand;\r\n\t\tif ( Any( \u0022toe\u0022, \u0022foot\u0022, \u0022ball_\u0022 ) ) return BodyRegion.Foot;\r\n\t\tif ( Any( \u0022head\u0022, \u0022eye\u0022, \u0022jaw\u0022, \u0022face\u0022 ) ) return BodyRegion.Head;\r\n\t\tif ( Any( \u0022neck\u0022 ) ) return BodyRegion.Neck;\r\n\t\tif ( Any( \u0022arm_lower\u0022, \u0022forearm\u0022, \u0022lowerarm\u0022, \u0022lower_arm\u0022, \u0022elbow\u0022 ) ) return BodyRegion.LowerArm;\r\n\t\tif ( Any( \u0022arm_upper\u0022, \u0022upperarm\u0022, \u0022upper_arm\u0022 ) ) return BodyRegion.UpperArm;\r\n\t\tif ( Any( \u0022leg_lower\u0022, \u0022calf\u0022, \u0022shin\u0022, \u0022lowerleg\u0022, \u0022lower_leg\u0022, \u0022knee\u0022 ) ) return BodyRegion.LowerLeg;\r\n\t\tif ( Any( \u0022leg_upper\u0022, \u0022thigh\u0022, \u0022upperleg\u0022, \u0022upper_leg\u0022 ) ) return BodyRegion.UpperLeg;\r\n\t\tif ( Any( \u0022pelvis\u0022, \u0022hip\u0022 ) ) return BodyRegion.Hips;\r\n\t\tif ( Any( \u0022spine\u0022, \u0022chest\u0022, \u0022clavicle\u0022, \u0022torso\u0022, \u0022abdomen\u0022, \u0022belly\u0022, \u0022collar\u0022, \u0022shoulder\u0022 ) ) return BodyRegion.Torso;\r\n\t\tif ( Any( \u0022arm\u0022 ) ) return BodyRegion.UpperArm;\r\n\t\tif ( Any( \u0022leg\u0022 ) ) return BodyRegion.UpperLeg;\r\n\r\n\t\treturn BodyRegion.None;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The region nearest a point, and how far along that region\u0027s bone it sits.\r\n\t///\r\n\t/// \u0022How far along\u0022 is measured on the nearest bone of the region, and a region with a chain of\r\n\t/// bones (a spine, a twist chain) is measured along the whole chain, so 0.5 is halfway down the\r\n\t/// upper arm whether that arm is one bone or three.\r\n\t/// \u003C/summary\u003E\r\n\tpublic sealed class Map\r\n\t{\r\n\t\treadonly List\u003C(BodyRegion Region, Vec3 A, Vec3 B)\u003E _segments = new();\r\n\t\treadonly List\u003Cint\u003E _segmentBones = new();\r\n\r\n\t\tpublic int Count =\u003E _segments.Count;\r\n\r\n\t\t/// \u003Csummary\u003EEvery region bone as a segment, for fitting capsules to.\u003C/summary\u003E\r\n\t\tpublic IReadOnlyList\u003C(BodyRegion Region, Vec3 A, Vec3 B)\u003E Segments =\u003E _segments;\r\n\r\n\t\t/// \u003Csummary\u003EThe skeleton bone each of \u003Csee cref=\u0022Segments\u0022/\u003E came from.\u003C/summary\u003E\r\n\t\tpublic IReadOnlyList\u003Cint\u003E SegmentBones =\u003E _segmentBones;\r\n\r\n\t\tpublic Map( Skeleton skeleton )\r\n\t\t{\r\n\r\n\t\t\tfor ( var i = 0; i \u003C skeleton.Count; i\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tvar region = Classify( skeleton.Bones[i].Name );\r\n\r\n\t\t\t\tif ( region == BodyRegion.None )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tvar a = skeleton.HeadWorld( i );\r\n\t\t\t\tvar b = skeleton.TailWorld( i );\r\n\r\n\t\t\t\tif ( (b - a).LengthSquared \u003C 1e-8f )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\t_segments.Add( (region, a, b) );\r\n\t\t\t\t_segmentBones.Add( i );\r\n\t\t\t}\r\n\r\n\t\t}\r\n\r\n\t\t/// \u003Csummary\u003ENearest region to \u003Cparamref name=\u0022p\u0022/\u003E, and t along it (0 at the joint nearest\r\n\t\t/// the body\u0027s centre, 1 at the far end).\u003C/summary\u003E\r\n\t\tpublic (BodyRegion Region, float T) Locate( Vec3 p ) =\u003E Locate( p, BodyRegion.None );\r\n\r\n\t\t/// \u003Csummary\u003EAs \u003Csee cref=\u0022Locate(Vec3)\u0022/\u003E, but only among \u003Cparamref name=\u0022only\u0022/\u003E\u0027s bones -\r\n\t\t/// for a surface that already knows which region it belongs to, such as an envelope hull.\r\n\t\t/// None means any region.\u003C/summary\u003E\r\n\t\tpublic (BodyRegion Region, float T) Locate( Vec3 p, BodyRegion only )\r\n\t\t{\r\n\t\t\tvar best = float.MaxValue;\r\n\t\t\tvar region = BodyRegion.None;\r\n\t\t\tvar chainT = 0f;\r\n\r\n\t\t\tforeach ( var (r, a, b ) in _segments )\r\n\t\t\t{\r\n\t\t\t\tif ( only != BodyRegion.None \u0026\u0026 r != only )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tvar ab = b - a;\r\n\t\t\t\tvar t = Math.Clamp( Vec3.Dot( p - a, ab ) / ab.LengthSquared, 0f, 1f );\r\n\t\t\t\tvar d = (a \u002B ab * t - p).LengthSquared;\r\n\r\n\t\t\t\tif ( d \u003C best )\r\n\t\t\t\t{\r\n\t\t\t\t\tbest = d;\r\n\t\t\t\t\tregion = r;\r\n\t\t\t\t\tchainT = t;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tif ( region == BodyRegion.None )\r\n\t\t\t\treturn (region, 0f);\r\n\r\n\t\t\t// Chains: measure t over the whole region on this side, start to end, so a limb reads\r\n\t\t\t// 0..1 shoulder to wrist whether it is one bone or a bone with twist bones laid over it\r\n\t\t\t// (Citizen\u0027s are - summing their lengths would count the arm twice).\r\n\t\t\tif ( Chain( region, Side( p ), out var from, out var to ) )\r\n\t\t\t{\r\n\t\t\t\tvar axis = to - from;\r\n\t\t\t\tchainT = Math.Clamp( Vec3.Dot( p - from, axis ) / axis.LengthSquared, 0f, 1f );\r\n\t\t\t}\r\n\r\n\t\t\t// The head is measured bottom-up (a beanie covers the TOP), the torso and hips top-down\r\n\t\t\t// (a hem reads down from the collar).\r\n\t\t\tif ( region == BodyRegion.Head )\r\n\t\t\t\tchainT = 1f - chainT;\r\n\r\n\t\t\treturn (region, chainT);\r\n\t\t}\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// The line a region runs along, on one side of the body: from the end nearest the body\u0027s\r\n\t\t/// centre (the top, for anything on the spine or a leg) to the far end. This is the axis t\r\n\t\t/// is measured on and the axis a limb\u0027s UVs wrap around, so both agree about where a\r\n\t\t/// sleeve starts. False for a region the skeleton does not have.\r\n\t\t/// \u003C/summary\u003E\r\n\t\tpublic bool Chain( BodyRegion region, int side, out Vec3 from, out Vec3 to )\r\n\t\t{\r\n\t\t\tfrom = to = Vec3.Zero;\r\n\t\t\tvar ends = new List\u003CVec3\u003E();\r\n\r\n\t\t\tforeach ( var s in _segments )\r\n\t\t\t{\r\n\t\t\t\tif ( s.Region != region || (!IsCentral( region ) \u0026\u0026 Side( (s.A \u002B s.B) * 0.5f ) != side) )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tends.Add( s.A );\r\n\t\t\t\tends.Add( s.B );\r\n\t\t\t}\r\n\r\n\t\t\tif ( ends.Count \u003C 2 )\r\n\t\t\t\treturn false;\r\n\r\n\t\t\tfrom = IsCentral( region ) || region is BodyRegion.UpperLeg or BodyRegion.LowerLeg or BodyRegion.Foot\r\n\t\t\t\t? ends.OrderByDescending( e =\u003E e.z ).First()\r\n\t\t\t\t: ends.OrderBy( e =\u003E MathF.Abs( e.y ) ).First();\r\n\r\n\t\t\tvar start = from;\r\n\t\t\tto = ends.OrderByDescending( e =\u003E (e - start).LengthSquared ).First();\r\n\r\n\t\t\treturn (to - from).LengthSquared \u003E 1e-8f;\r\n\t\t}\r\n\r\n\t\tpublic static bool IsCentral( BodyRegion r ) =\u003E r is BodyRegion.Head or BodyRegion.Neck or BodyRegion.Torso or BodyRegion.Hips;\r\n\r\n\t\tstatic int Side( Vec3 p ) =\u003E p.y \u003E 0 ? 1 : -1;\r\n\r\n\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Make a garment that fits a particular body.\r\n///\r\n/// THE BODY DOES MOST OF THE WORK. The rig only says which faces a recipe covers; the garment is then\r\n/// lifted off the body\u0027s own surface, so it starts with that body\u0027s proportions and topology - which\r\n/// is also the topology the s\u0026box quality bar asks clothing to follow. After that:\r\n///\r\n/// 1. every vertex is pushed out to the fit offset along its normal,\r\n/// 2. collision: any vertex closer than the clearance to ANY body - the chest pressing into an arm, a\r\n///    collar into a cog - is pushed back out along that surface\u0027s normal, and smoothed, a few times,\r\n/// 3. the openings flare out if asked,\r\n/// 4. the sheet is given thickness with \u003Csee cref=\u0022MeshSolidify\u0022/\u003E.\r\n///\r\n/// It does not drape. Folds come from the flare and from sculpting until editor cloth arrives.\r\n/// \u003C/summary\u003E\r\npublic static class GarmentFit\r\n{\r\n\tpublic sealed class Options\r\n\t{\r\n\t\t/// \u003Csummary\u003ERound colliders for the drape instead of the bodies\u0027 triangles. See Capsule.\r\n\t\t/// Null drapes against the meshes, as before.\u003C/summary\u003E\r\n\t\tpublic List\u003CCapsule\u003E Capsules;\r\n\r\n\t\t/// \u003Csummary\u003EHow far the drape may move the cloth from where the fit put it. 0 takes the\r\n\t\t/// default: a couple of inches, room to settle and no more.\u003C/summary\u003E\r\n\t\tpublic float Tether;\r\n\r\n\t\t/// \u003Csummary\u003EHow far off the body the garment sits, in model units (inches on the Citizen).\u003C/summary\u003E\r\n\t\tpublic float Offset = 0.4f;\r\n\r\n\t\t/// \u003Csummary\u003EThe closest any part of the garment may come to any body, after collision.\u003C/summary\u003E\r\n\t\tpublic float Clearance = 0.15f;\r\n\r\n\t\t/// \u003Csummary\u003EHow far the openings (hems, cuffs) flare out beyond the offset.\u003C/summary\u003E\r\n\t\tpublic float Flare;\r\n\r\n\t\t/// \u003Csummary\u003EHow far up from an opening the flare reaches.\u003C/summary\u003E\r\n\t\tpublic float FlareReach = 4f;\r\n\r\n\t\t/// \u003Csummary\u003ECloth thickness; 0 leaves a single-sided sheet.\u003C/summary\u003E\r\n\t\tpublic float Thickness = 0.12f;\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// Inflate the garment along its own normals, in inches. Puffed sleeves, a padded jacket,\r\n\t\t/// a quilted vest.\r\n\t\t///\r\n\t\t/// Marvelous Designer calls this Pressure and Simply Cloth calls it the same; it is on both\r\n\t\t/// tools\u0027 front page because it is the cheapest way to turn a fitted shape into a different\r\n\t\t/// GARMENT rather than the same garment at a different size. Looseness moves the whole\r\n\t\t/// surface away from the body uniformly, which reads as a bigger shirt. This pushes along\r\n\t\t/// the local normal, which reads as stuffing.\r\n\t\t/// \u003C/summary\u003E\r\n\t\tpublic float Puff;\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// Pull the openings in towards their own centres, 0 none to 1 closed. A waistband, elastic\r\n\t\t/// cuffs, a gathered ankle on a jogger.\r\n\t\t///\r\n\t\t/// The hems are the only part of a garment that has nothing holding it in, so they are the\r\n\t\t/// part that reads as loose whatever the rest is doing - which is why \u0022tighten the waistband\u0022\r\n\t\t/// is a thing people ask for and \u0022make the shirt smaller\u0022 is not the same request.\r\n\t\t/// \u003C/summary\u003E\r\n\t\tpublic float Cinch;\r\n\r\n\t\t/// \u003Csummary\u003EHow far up from an opening the cinch reaches, in inches. A waistband is an inch;\r\n\t\t/// a gathered ankle is three.\u003C/summary\u003E\r\n\t\tpublic float CinchReach = 1.5f;\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// Creases along the surface, 0 smooth to 1 rumpled.\r\n\t\t///\r\n\t\t/// NOT A SUBSTITUTE FOR THE DRAPE. The solver makes the folds that gravity and collision\r\n\t\t/// actually produce, and those are the ones that sit in the right places. This is the fine\r\n\t\t/// noise on top - the crumple of cloth that has been worn - which a solver would need a much\r\n\t\t/// finer mesh and much longer to find, for something nobody looks at closely enough to tell\r\n\t\t/// apart from noise.\r\n\t\t/// \u003C/summary\u003E\r\n\t\tpublic float Wrinkles;\r\n\r\n\t\t/// \u003Csummary\u003ESize of a wrinkle, in inches. Small is linen, large is a heavy coat.\u003C/summary\u003E\r\n\t\tpublic float WrinkleScale = 2.2f;\r\n\r\n\t\t/// \u003Csummary\u003ECollision passes. Each pass pushes out and relaxes once.\u003C/summary\u003E\r\n\t\tpublic int Passes = 4;\r\n\r\n\t\tpublic int MaterialSlot;\r\n\r\n\t\t/// \u003Csummary\u003ELet the garment settle under gravity against the bodies before it is thickened.\u003C/summary\u003E\r\n\t\tpublic bool Drape = true;\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// What the cloth is made of, as a set of solver settings.\r\n\t\t///\r\n\t\t/// THE FIRST THING EITHER OF THE BIG TOOLS ASKS. Marvelous Designer opens on a fabric\r\n\t\t/// library and Simply Cloth ships presets, because the difference between cotton and leather\r\n\t\t/// is most of the difference between two garments of the same shape - it decides whether the\r\n\t\t/// folds are many and soft or few and standing. Stiffness alone could only say how much a\r\n\t\t/// garment resists being stretched, which is the one property people do not think in.\r\n\t\t///\r\n\t\t/// A STARTING POINT, NOT A MEASUREMENT. These are artistic presets tuned to look right at\r\n\t\t/// this scale, not the physical constants of real textiles; Stiffness still overrides the\r\n\t\t/// stretch on top of whichever is chosen.\r\n\t\t/// \u003C/summary\u003E\r\n\t\tpublic Fabric Fabric = Fabric.Cotton;\r\n\r\n\t\t/// \u003Csummary\u003ESimulation steps, at 1/60 s each. More settles further and costs more.\u003C/summary\u003E\r\n\t\tpublic int DrapeSteps = 40;\r\n\r\n\t\t/// \u003Csummary\u003E0 = stretchy jersey, 1 = stiff canvas. How hard the edges hold their fitted length.\u003C/summary\u003E\r\n\t\tpublic float Stiffness = 0.8f;\r\n\r\n\t\t/// \u003Csummary\u003EPin the top fraction of the garment (by height) where it is, so trousers keep their\r\n\t\t/// waistband. 0 pins nothing - a top rests on the shoulders by collision alone.\u003C/summary\u003E\r\n\t\tpublic float PinTop;\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// Stop the garment passing through itself where it folds. OFF by default, and deliberately:\r\n\t\t/// it is several times the cost of the whole rest of the drape, and this runs on every\r\n\t\t/// rebuild while a slider moves. The fit pass has already pushed the garment out of the body,\r\n\t\t/// so what this buys is folds resolving against each other - worth asking for on a finished\r\n\t\t/// garment, not worth paying for on every drag.\r\n\t\t/// \u003C/summary\u003E\r\n\t\tpublic bool SelfCollision;\r\n\t}\r\n\r\n\tpublic sealed class Result\r\n\t{\r\n\t\tpublic PolyMesh Mesh;\r\n\t\tpublic int SourceFaces;\r\n\t\tpublic int DroppedInside;\r\n\t\tpublic bool Solidified;\r\n\t\tpublic string SolidifyProblem;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Build \u003Cparamref name=\u0022recipe\u0022/\u003E on \u003Cparamref name=\u0022bodies\u0022/\u003E, located with \u003Cparamref name=\u0022map\u0022/\u003E.\r\n\t/// Returns null when the recipe covers no face at all.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static Result Fit( IReadOnlyList\u003CPolyMesh\u003E wearers, BodyRegions.Map map, GarmentRecipe recipe, Options o,\r\n\t\tIReadOnlyList\u003CPolyMesh\u003E otherGarments = null, IReadOnlyList\u003CBodyRegion\u003E wearerRegions = null )\r\n\t{\r\n\t\t// Everything collides; only the wearers are cut from.\r\n\t\tvar bodies = wearers.Concat( otherGarments ?? Array.Empty\u003CPolyMesh\u003E() ).ToList();\r\n\t\tvar trees = bodies.Select( MeshBVH.Build ).ToList();\r\n\t\tvar closed = bodies.Select( b =\u003E MeshValidator.Validate( b ).IsClosed ).ToList();\r\n\t\tvar result = new Result { Mesh = new PolyMesh() };\r\n\t\tvar garment = result.Mesh;\r\n\t\tvar remap = new Dictionary\u003C(int Body, int Vertex), int\u003E();\r\n\t\tvar cutRemap = new Dictionary\u003C(int Body, int A, int B, BodyRegion Region, int End), int\u003E();\r\n\r\n\t\tfor ( var bi = 0; bi \u003C wearers.Count; bi\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar body = bodies[bi];\r\n\r\n\t\t\t// A wearer that already knows its region - an envelope hull - keeps it, and is only\r\n\t\t\t// measured along it. Left to the nearest bone, the front of Camhead\u0027s torso hull was\r\n\t\t\t// claimed by the arms hanging beside it and cut out of the shirt.\r\n\t\t\tvar only = wearerRegions is not null \u0026\u0026 bi \u003C wearerRegions.Count ? wearerRegions[bi] : BodyRegion.None;\r\n\r\n\t\t\tint Vertex( int vi )\r\n\t\t\t{\r\n\t\t\t\tvar key = (bi, vi);\r\n\r\n\t\t\t\tif ( !remap.TryGetValue( key, out var index ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tindex = garment.AddVertex( body.Positions[vi] );\r\n\t\t\t\t\tremap[key] = index;\r\n\t\t\t\t}\r\n\r\n\t\t\t\treturn index;\r\n\t\t\t}\r\n\r\n\t\t\tfor ( var fi = 0; fi \u003C body.FaceCount; fi\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tvar face = body.Faces[fi];\r\n\r\n\t\t\t\tif ( face.Count \u003C 3 )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tvar centroid = body.FaceCentroid( face );\r\n\t\t\t\tvar (region, t) = map.Locate( centroid, only );\r\n\r\n\t\t\t\t// The side is the face\u0027s own position, not the bone\u0027s: \u002By is the body\u0027s left, the\r\n\t\t\t\t// way Source measures it. Taken from the face rather than from the bone name so an\r\n\t\t\t\t// imported skeleton with no _L / _R convention still makes a one-sleeved shirt.\r\n\t\t\t\tvar side = centroid.y \u003E 0 ? 1 : -1;\r\n\t\t\t\tvar spans = recipe.Spans.Where( s =\u003E s.Region == region \u0026\u0026 (s.Side == 0 || s.Side == side) ).ToList();\r\n\r\n\t\t\t\tif ( spans.Count == 0 )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tstatic bool Within( (BodyRegion Region, float From, float To, int Side) s, float t ) =\u003E\r\n\t\t\t\t\tt \u003E= s.From - 1e-4f \u0026\u0026 t \u003C= s.To \u002B 1e-4f;\r\n\r\n\t\t\t\t// THE FACE IS CUT AT THE HEM, not kept or dropped whole. Whole faces gave every\r\n\t\t\t\t// opening a sawtooth edge one face deep - the jagged cuffs and collars that said\r\n\t\t\t\t// \u0022generated\u0022 from across the room - and no amount of relaxing put a straight line\r\n\t\t\t\t// back. The span\u0027s two ends are two planes across the bone (t is linear along it),\r\n\t\t\t\t// and the polygon is clipped to each (Sutherland-Hodgman), so the edge is a clean\r\n\t\t\t\t// line round the limb. An end at 0 or 1 is open - a sleeve runs into the shoulder,\r\n\t\t\t\t// a glove past the fingertips - and is not cut. A face is in if any corner of it\r\n\t\t\t\t// is, and it is the span round the centre, or failing that round a corner, that\r\n\t\t\t\t// does the cutting.\r\n\t\t\t\tvar nodes = new List\u003CCutNode\u003E( face.Count \u002B 2 );\r\n\t\t\t\tvar span = spans[0];\r\n\t\t\t\tvar chained = map.Chain( region, side, out var from, out var to );\r\n\r\n\t\t\t\tif ( chained )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar axis = to - from;\r\n\t\t\t\t\tvar lengthSq = axis.LengthSquared;\r\n\r\n\t\t\t\t\tfor ( var c = 0; c \u003C face.Count; c\u002B\u002B )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tvar vi = face.Indices[c];\r\n\t\t\t\t\t\tvar p = body.Positions[vi];\r\n\t\t\t\t\t\tnodes.Add( new CutNode( vi, vi, -1, p, Vec3.Dot( p - from, axis ) / lengthSq ) );\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tvar covering = spans.FirstOrDefault( s =\u003E Within( s, t ) );\r\n\r\n\t\t\t\t\tif ( covering.Region != region )\r\n\t\t\t\t\t\tcovering = spans.FirstOrDefault( s =\u003E nodes.Any( n =\u003E Within( s, n.T ) ) );\r\n\r\n\t\t\t\t\tif ( covering.Region != region )\r\n\t\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\t\tspan = covering;\r\n\t\t\t\t}\r\n\t\t\t\telse if ( !spans.Any( s =\u003E Within( s, t ) ) )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\t// A face buried inside another part - an arm cylinder\u0027s end inside the torso block,\r\n\t\t\t\t// the way CAD characters are built - would be pushed out onto the surface as a crumpled\r\n\t\t\t\t// flap. It is not skin; skip it.\r\n\t\t\t\tif ( InsideOther( bodies, trees, closed, bi, centroid ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tresult.DroppedInside\u002B\u002B;\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif ( chained )\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( span.From \u003E 0f )\r\n\t\t\t\t\t\tnodes = ClipSpan( nodes, span.From, keepAbove: true, end: 0 );\r\n\r\n\t\t\t\t\tif ( span.To \u003C 1f \u0026\u0026 nodes.Count \u003E= 3 )\r\n\t\t\t\t\t\tnodes = ClipSpan( nodes, span.To, keepAbove: false, end: 1 );\r\n\r\n\t\t\t\t\tif ( nodes.Count \u003C 3 )\r\n\t\t\t\t\t\tcontinue;\r\n\t\t\t\t}\r\n\t\t\t\telse\r\n\t\t\t\t{\r\n\t\t\t\t\tfor ( var c = 0; c \u003C face.Count; c\u002B\u002B )\r\n\t\t\t\t\t\tnodes.Add( new CutNode( face.Indices[c], face.Indices[c], -1, body.Positions[face.Indices[c]], 0f ) );\r\n\t\t\t\t}\r\n\r\n\t\t\t\tvar indices = new int[nodes.Count];\r\n\r\n\t\t\t\tfor ( var c = 0; c \u003C nodes.Count; c\u002B\u002B )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar node = nodes[c];\r\n\r\n\t\t\t\t\t// An original corner; or a cut point on an original edge, which the face on the\r\n\t\t\t\t\t// other side of that edge finds again by the same key, so the cut is watertight.\r\n\t\t\t\t\tif ( node.End \u003C 0 )\r\n\t\t\t\t\t\tindices[c] = Vertex( node.A );\r\n\t\t\t\t\telse if ( node.A \u003E= 0 )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tvar (lo, hi) = node.A \u003C node.B ? (node.A, node.B) : (node.B, node.A);\r\n\t\t\t\t\t\tvar key = (bi, lo, hi, region, node.End);\r\n\r\n\t\t\t\t\t\tif ( !cutRemap.TryGetValue( key, out var index ) )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tindex = garment.AddVertex( node.P );\r\n\t\t\t\t\t\t\tcutRemap[key] = index;\r\n\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\tindices[c] = index;\r\n\t\t\t\t\t}\r\n\t\t\t\t\telse\r\n\t\t\t\t\t\tindices[c] = garment.AddVertex( node.P );\r\n\t\t\t\t}\r\n\r\n\t\t\t\tgarment.AddFace( indices, null, o.MaterialSlot );\r\n\t\t\t\tresult.SourceFaces\u002B\u002B;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( garment.FaceCount == 0 )\r\n\t\t\treturn null;\r\n\r\n\t\tvar start = garment.Positions.ToArray();\r\n\t\tvar normals = garment.ComputeVertexNormals();\r\n\t\tvar boundary = BoundaryVertices( garment );\r\n\t\tvar flare = FlareWeights( garment, boundary, o.FlareReach );\r\n\r\n\t\t// 1. Out to the fit offset, flaring toward the openings.\r\n\t\tfor ( var i = 0; i \u003C garment.VertexCount; i\u002B\u002B )\r\n\t\t\tgarment.Positions[i] = start[i] \u002B normals[i] * (o.Offset \u002B o.Flare * flare[i]);\r\n\r\n\t\t// 2. Collision against every body, with a relax between passes so a push does not leave a dent.\r\n\t\tvar neighbours = Neighbours( garment );\r\n\t\tvar reach = o.Offset \u002B o.Flare \u002B o.Clearance \u002B 2f;\r\n\r\n\t\tfor ( var pass = 0; pass \u003C Math.Max( 1, o.Passes ); pass\u002B\u002B )\r\n\t\t{\r\n\t\t\tPushOut( garment, bodies, trees, o.Clearance, reach );\r\n\t\t\tRelax( garment, neighbours, boundary, 0.35f );\r\n\t\t}\r\n\r\n\t\tPushOut( garment, bodies, trees, o.Clearance, reach );\r\n\r\n\t\tif ( o.Drape \u0026\u0026 o.DrapeSteps \u003E 0 )\r\n\t\t\tDrape( garment, bodies, o );\r\n\r\n\t\t// SHAPING COMES AFTER THE DRAPE AND BEFORE THE THICKNESS, and the order is the whole\r\n\t\t// correctness argument. After the drape, because these are deliberate style decisions and\r\n\t\t// the solver would flatten a puff straight back out as if it were an error. Before the\r\n\t\t// thickness, because Solidify offsets the surface it is given - shaping a solid would move\r\n\t\t// its inner and outer shells apart by different amounts and split the garment open.\r\n\t\t//\r\n\t\t// Then pushed out again: a puff or a wrinkle can drive the cloth back into the body, and a\r\n\t\t// garment that clips is worse than one that is not quite as puffy as asked.\r\n\t\tif ( o.Puff \u003E 0f )\r\n\t\t\tInflate( garment, o.Puff );\r\n\r\n\t\tif ( o.Cinch \u003E 0f )\r\n\t\t\tCinch( garment, o.Cinch, o.CinchReach );\r\n\r\n\t\tif ( o.Wrinkles \u003E 0f )\r\n\t\t\tWrinkle( garment, o.Wrinkles, o.WrinkleScale );\r\n\r\n\t\tif ( o.Puff \u003E 0f || o.Wrinkles \u003E 0f )\r\n\t\t\tPushOut( garment, bodies, trees, o.Clearance, reach );\r\n\r\n\t\tLimbUVs( garment, map );\r\n\r\n\t\tif ( o.Thickness \u003E 0f )\r\n\t\t{\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tgarment = MeshSolidify.Solidify( garment, o.Thickness );\r\n\t\t\t\tresult.Solidified = true;\r\n\t\t\t}\r\n\t\t\tcatch ( Exception e )\r\n\t\t\t{\r\n\t\t\t\tresult.SolidifyProblem = e.Message;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tresult.Mesh = garment;\r\n\t\treturn result;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EA corner of a face being cut: an original vertex (A == B, End -1), or a point on\r\n\t/// the original edge A-B where the span\u0027s end (0 the start, 1 the finish) crosses it. A is -1\r\n\t/// for a point on an edge the cut itself made, which no other face shares.\u003C/summary\u003E\r\n\treadonly record struct CutNode( int A, int B, int End, Vec3 P, float T );\r\n\r\n\t/// \u003Csummary\u003EOne Sutherland-Hodgman pass: keep the side of t = limit the span is on.\u003C/summary\u003E\r\n\tstatic List\u003CCutNode\u003E ClipSpan( List\u003CCutNode\u003E nodes, float limit, bool keepAbove, int end )\r\n\t{\r\n\t\tvar kept = new List\u003CCutNode\u003E( nodes.Count \u002B 2 );\r\n\r\n\t\tfor ( var i = 0; i \u003C nodes.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar cur = nodes[i];\r\n\t\t\tvar nxt = nodes[(i \u002B 1) % nodes.Count];\r\n\t\t\tvar inCur = keepAbove ? cur.T \u003E= limit : cur.T \u003C= limit;\r\n\t\t\tvar inNxt = keepAbove ? nxt.T \u003E= limit : nxt.T \u003C= limit;\r\n\r\n\t\t\tif ( inCur )\r\n\t\t\t\tkept.Add( cur );\r\n\r\n\t\t\tif ( inCur == inNxt )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar s = Math.Clamp( (limit - cur.T) / (nxt.T - cur.T), 0f, 1f );\r\n\t\t\tvar (a, b) = EdgeUnder( cur, nxt );\r\n\t\t\tkept.Add( new CutNode( a, b, end, cur.P \u002B (nxt.P - cur.P) * s, limit ) );\r\n\t\t}\r\n\r\n\t\treturn kept;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe original edge a segment between two nodes lies on, or (-1, -1) when it is a\r\n\t/// line the first cut drew across the face and nothing else can share.\u003C/summary\u003E\r\n\tstatic (int, int) EdgeUnder( CutNode x, CutNode y )\r\n\t{\r\n\t\tstatic bool OnEdge( CutNode cut, int vertex ) =\u003E cut.A == vertex || cut.B == vertex;\r\n\r\n\t\tif ( x.End \u003C 0 \u0026\u0026 y.End \u003C 0 )\r\n\t\t\treturn (x.A, y.A);\r\n\r\n\t\tif ( x.End \u003C 0 \u0026\u0026 y.A \u003E= 0 \u0026\u0026 OnEdge( y, x.A ) )\r\n\t\t\treturn (y.A, y.B);\r\n\r\n\t\tif ( y.End \u003C 0 \u0026\u0026 x.A \u003E= 0 \u0026\u0026 OnEdge( x, y.A ) )\r\n\t\t\treturn (x.A, x.B);\r\n\r\n\t\tif ( x.End \u003E= 0 \u0026\u0026 y.End \u003E= 0 \u0026\u0026 x.A \u003E= 0 \u0026\u0026 x.A == y.A \u0026\u0026 x.B == y.B )\r\n\t\t\treturn (x.A, x.B);\r\n\r\n\t\treturn (-1, -1);\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Push every vertex out along its own normal. Pressure, in Marvelous Designer\u0027s terms.\r\n\t///\r\n\t/// The normals are computed ONCE, from the shape as it arrives, rather than per vertex as it\r\n\t/// moves. Recomputing as you go makes a bulge feed itself - the more a region inflates the more\r\n\t/// outward its neighbours\u0027 normals become - and a padded jacket turns into a balloon.\r\n\t/// \u003C/summary\u003E\r\n\tstatic void Inflate( PolyMesh garment, float amount )\r\n\t{\r\n\t\tvar normals = garment.ComputeVertexNormals();\r\n\r\n\t\tfor ( var i = 0; i \u003C garment.VertexCount; i\u002B\u002B )\r\n\t\t\tgarment.Positions[i] \u002B= normals[i] * amount;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Pull the openings in towards their own centres: waistbands, elastic cuffs, gathered ankles.\r\n\t///\r\n\t/// The pull falls off with distance from the edge, so the garment is gathered at the hem and\r\n\t/// untouched an inch above it - which is what elastic in a casing actually does, and what makes\r\n\t/// this read as a waistband rather than as a garment that tapers.\r\n\t/// \u003C/summary\u003E\r\n\tstatic void Cinch( PolyMesh garment, float amount, float reach )\r\n\t{\r\n\t\tvar loops = GarmentTrim.OrderedBoundaryLoops( garment );\r\n\r\n\t\tif ( loops.Count == 0 || reach \u003C= 1e-4f )\r\n\t\t\treturn;\r\n\r\n\t\tamount = Math.Clamp( amount, 0f, 1f );\r\n\r\n\t\t// Every vertex moves towards the centre of the NEAREST opening, weighted by how close it is\r\n\t\t// to that opening\u0027s edge. Nearest rather than \u0022the opening it belongs to\u0022: a garment has no\r\n\t\t// notion of which panel a vertex is on, and distance answers the question correctly anyway.\r\n\t\tvar centres = new List\u003CVec3\u003E();\r\n\t\tvar rings = new List\u003CList\u003CVec3\u003E\u003E();\r\n\r\n\t\tforeach ( var loop in loops )\r\n\t\t{\r\n\t\t\tvar points = loop.Select( v =\u003E garment.Positions[v] ).ToList();\r\n\t\t\tvar centre = Vec3.Zero;\r\n\r\n\t\t\tforeach ( var p in points )\r\n\t\t\t\tcentre \u002B= p;\r\n\r\n\t\t\tcentres.Add( centre / points.Count );\r\n\t\t\trings.Add( points );\r\n\t\t}\r\n\r\n\t\tfor ( var i = 0; i \u003C garment.VertexCount; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar p = garment.Positions[i];\r\n\t\t\tvar best = float.MaxValue;\r\n\t\t\tvar which = -1;\r\n\r\n\t\t\tfor ( var li = 0; li \u003C rings.Count; li\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tforeach ( var q in rings[li] )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar d = (q - p).LengthSquared;\r\n\r\n\t\t\t\t\tif ( d \u003E= best )\r\n\t\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\t\tbest = d;\r\n\t\t\t\t\twhich = li;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tif ( which \u003C 0 )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar distance = MathF.Sqrt( best );\r\n\r\n\t\t\tif ( distance \u003E= reach )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\t// Smoothstep rather than linear, so the top of the band blends into the garment instead\r\n\t\t\t// of leaving a crease exactly \u0060reach\u0060 inches up.\r\n\t\t\tvar t = 1f - distance / reach;\r\n\t\t\tvar falloff = t * t * (3f - 2f * t);\r\n\r\n\t\t\tvar centre = centres[which];\r\n\r\n\t\t\t// Along the opening\u0027s own plane only: pulling towards the centre POINT would drag the\r\n\t\t\t// hem up the body as well as in, and shorten the garment as a side effect of tightening\r\n\t\t\t// it. Only the part of the offset perpendicular to the ring\u0027s axis is wanted.\r\n\t\t\tvar toCentre = centre - p;\r\n\t\t\tvar axis = RingAxis( rings[which], centre );\r\n\r\n\t\t\ttoCentre -= axis * Vec3.Dot( toCentre, axis );\r\n\r\n\t\t\tgarment.Positions[i] = p \u002B toCentre * (amount * falloff);\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe direction an opening faces: the normal of the plane its vertices best lie in.\r\n\t/// Found by summing the fan of cross products around the ring, which is the ring\u0027s own area\r\n\t/// vector and needs no fitting.\u003C/summary\u003E\r\n\tstatic Vec3 RingAxis( List\u003CVec3\u003E ring, Vec3 centre )\r\n\t{\r\n\t\tvar area = Vec3.Zero;\r\n\r\n\t\tfor ( var i = 0; i \u003C ring.Count; i\u002B\u002B )\r\n\t\t\tarea \u002B= Vec3.Cross( ring[i] - centre, ring[(i \u002B 1) % ring.Count] - centre );\r\n\r\n\t\treturn area.LengthSquared \u003C 1e-8f ? new Vec3( 0, 0, 1 ) : area.Normal;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Fine creases along the surface. See Options.Wrinkles for what this is and is not.\r\n\t///\r\n\t/// Three octaves of a cheap value noise over the vertex\u0027s own position, displaced along the\r\n\t/// normal. Position-based rather than UV-based on purpose: UVs are laid out per panel, so a\r\n\t/// UV-space wrinkle would stop dead at every seam, and cloth does not.\r\n\t/// \u003C/summary\u003E\r\n\tstatic void Wrinkle( PolyMesh garment, float amount, float scale )\r\n\t{\r\n\t\tif ( scale \u003C= 1e-3f )\r\n\t\t\treturn;\r\n\r\n\t\tvar normals = garment.ComputeVertexNormals();\r\n\r\n\t\tfor ( var i = 0; i \u003C garment.VertexCount; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar p = garment.Positions[i] / scale;\r\n\r\n\t\t\tvar n = Noise( p ) * 0.6f\r\n\t\t\t\t\u002B Noise( p * 2.3f ) * 0.3f\r\n\t\t\t\t\u002B Noise( p * 4.7f ) * 0.1f;\r\n\r\n\t\t\tgarment.Positions[i] \u002B= normals[i] * (n * amount * scale * 0.09f);\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EA cheap deterministic value noise in -1..1. Deterministic matters more than\r\n\t/// quality: the same garment must rebuild to the same wrinkles, or every rebuild would be a\r\n\t/// different model and undo would be a surprise.\u003C/summary\u003E\r\n\tstatic float Noise( Vec3 p )\r\n\t{\r\n\t\tvar x = MathF.Sin( p.x * 12.9898f \u002B p.y * 78.233f \u002B p.z * 37.719f ) * 43758.5453f;\r\n\t\tvar y = MathF.Sin( p.x * 93.9898f \u002B p.y * 67.345f \u002B p.z * 24.123f ) * 28001.8384f;\r\n\r\n\t\treturn (x - MathF.Floor( x )) \u002B (y - MathF.Floor( y )) - 1f;\r\n\t}\r\n\r\n\tstatic bool InsideOther( IReadOnlyList\u003CPolyMesh\u003E bodies, List\u003CMeshBVH\u003E trees, List\u003Cbool\u003E closed, int self, Vec3 p )\r\n\t{\r\n\t\tfor ( var bi = 0; bi \u003C bodies.Count; bi\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( bi == self || !closed[bi] )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tif ( trees[bi].NearestSurface( bodies[bi], p, 1e6f ) is not { } hit )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tif ( hit.Distance \u003E 1e-4f \u0026\u0026 Vec3.Dot( p - hit.Point, hit.Normal ) \u003C 0 )\r\n\t\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\tinternal static void PushOut( PolyMesh garment, IReadOnlyList\u003CPolyMesh\u003E bodies, List\u003CMeshBVH\u003E trees, float clearance, float reach )\r\n\t{\r\n\t\tfor ( var i = 0; i \u003C garment.VertexCount; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar p = garment.Positions[i];\r\n\r\n\t\t\tfor ( var bi = 0; bi \u003C bodies.Count; bi\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tif ( trees[bi].NearestSurface( bodies[bi], p, reach ) is not { } hit )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tvar along = Vec3.Dot( p - hit.Point, hit.Normal );\r\n\r\n\t\t\t\tif ( along \u003C clearance )\r\n\t\t\t\t\tp \u002B= hit.Normal * (clearance - along);\r\n\t\t\t}\r\n\r\n\t\t\tgarment.Positions[i] = p;\r\n\t\t}\r\n\t}\r\n\r\n\tstatic void Relax( PolyMesh mesh, List\u003Cint\u003E[] neighbours, bool[] boundary, float amount )\r\n\t{\r\n\t\tvar next = mesh.Positions.ToArray();\r\n\r\n\t\tfor ( var i = 0; i \u003C mesh.VertexCount; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar n = neighbours[i];\r\n\r\n\t\t\tif ( n.Count == 0 )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar avg = Vec3.Zero;\r\n\r\n\t\t\tforeach ( var j in n )\r\n\t\t\t\tavg \u002B= mesh.Positions[j];\r\n\r\n\t\t\tavg /= n.Count;\r\n\t\t\tnext[i] = Vec3.Lerp( mesh.Positions[i], avg, boundary[i] ? amount * 0.3f : amount );\r\n\t\t}\r\n\r\n\t\tfor ( var i = 0; i \u003C next.Length; i\u002B\u002B )\r\n\t\t\tmesh.Positions[i] = next[i];\r\n\t}\r\n\r\n\tstatic List\u003Cint\u003E[] Neighbours( PolyMesh mesh )\r\n\t{\r\n\t\tvar sets = new HashSet\u003Cint\u003E[mesh.VertexCount];\r\n\r\n\t\tfor ( var i = 0; i \u003C sets.Length; i\u002B\u002B )\r\n\t\t\tsets[i] = new HashSet\u003Cint\u003E();\r\n\r\n\t\tforeach ( var f in mesh.Faces )\r\n\t\t{\r\n\t\t\tfor ( var c = 0; c \u003C f.Count; c\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tvar a = f.Indices[c];\r\n\t\t\t\tvar b = f.Indices[(c \u002B 1) % f.Count];\r\n\t\t\t\tsets[a].Add( b );\r\n\t\t\t\tsets[b].Add( a );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn sets.Select( s =\u003E s.ToList() ).ToArray();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Let the fitted garment settle under gravity against the bodies, using the kernel\u0027s one cloth\r\n\t/// solver. \u003Csee cref=\u0022Options.Stiffness\u0022/\u003E becomes XPBD stretch compliance \u2014 1 is canvas that\r\n\t/// holds its fitted length, 0 is jersey that gives \u2014 and \u003Csee cref=\u0022Options.PinTop\u0022/\u003E pins the\r\n\t/// top slice of the mesh so a waistband stays where it was fitted.\r\n\t/// \u003C/summary\u003E\r\n\tstatic void Drape( PolyMesh garment, IReadOnlyList\u003CPolyMesh\u003E bodies, Options o )\r\n\t{\r\n\t\t// The fabric first, then the garment\u0027s own settings over the top. Order matters: the preset\r\n\t\t// is what the cloth IS and Stiffness is the adjustment somebody made to it, so the\r\n\t\t// adjustment has to win - the other way round, moving the slider would do nothing on any\r\n\t\t// fabric that sets its own stretch.\r\n\t\tvar sim = ClothSim.WithFabric( garment, PinnedTop( garment, o.PinTop ), o.Fabric );\r\n\r\n\t\tsim.Thickness = MathF.Max( o.Clearance, 1e-3f );\r\n\t\tsim.SelfCollision = o.SelfCollision;\r\n\r\n\t\t// Stiffness runs 0..1 and compliance runs rigid..slack over four decades, so the map is\r\n\t\t// exponential: 1 -\u003E 1e-7 (canvas), 0.5 -\u003E 1e-5, 0 -\u003E 1e-3 (jersey).\r\n\t\tvar stretch = MathF.Pow( 10f, -7f \u002B 4f * (1f - Math.Clamp( o.Stiffness, 0f, 1f )) );\r\n\r\n\t\t// Stretch is scaled rather than replaced, so a stretch fabric stays stretchier than denim at\r\n\t\t// the same slider position instead of every fabric collapsing onto one curve.\r\n\t\tsim.StretchCompliance = MathF.Max( sim.StretchCompliance, 1e-7f ) / 1e-5f * stretch;\r\n\t\tsim.BendCompliance = MathF.Max( sim.BendCompliance, sim.StretchCompliance * 10f );\r\n\t\t// The garment was fitted to lie where it should; the drape only settles it. Two inches, or\r\n\t\t// four clearances on a loose fit, is room for a hem to fall and nowhere near room to drop\r\n\t\t// through a body with gaps in it. See ClothSim.Tether.\r\n\t\tsim.Tether = o.Tether \u003E 0f ? o.Tether : MathF.Max( 2f, o.Clearance * 4f );\r\n\r\n\t\tif ( o.Capsules is { Count: \u003E 0 } )\r\n\t\t\tsim.Capsules.AddRange( o.Capsules );\r\n\t\telse\r\n\t\t\tsim.SetColliders( bodies );\r\n\t\tsim.Run( o.DrapeSteps / 60f );\r\n\r\n\t\tfor ( var i = 0; i \u003C garment.VertexCount; i\u002B\u002B )\r\n\t\t\tgarment.Positions[i] = sim.Positions[i];\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EVertices in the top \u003Cparamref name=\u0022fraction\u0022/\u003E of the mesh\u0027s height. Empty when the\r\n\t/// fraction is 0, which pins nothing and lets the garment rest on the body by collision alone.\u003C/summary\u003E\r\n\tstatic IEnumerable\u003Cint\u003E PinnedTop( PolyMesh mesh, float fraction )\r\n\t{\r\n\t\tfraction = Math.Clamp( fraction, 0f, 1f );\r\n\r\n\t\tif ( fraction \u003C= 0f || mesh.VertexCount == 0 )\r\n\t\t\tyield break;\r\n\r\n\t\tvar top = mesh.Positions.Max( p =\u003E p.z );\r\n\t\tvar bottom = mesh.Positions.Min( p =\u003E p.z );\r\n\t\tvar cut = top - (top - bottom) * fraction;\r\n\r\n\t\tfor ( var i = 0; i \u003C mesh.VertexCount; i\u002B\u002B )\r\n\t\t\tif ( mesh.Positions[i].z \u003E= cut )\r\n\t\t\t\tyield return i;\r\n\t}\r\n\r\n\tpublic static bool[] BoundaryVertices( PolyMesh mesh )\r\n\t{\r\n\t\tvar boundary = new bool[mesh.VertexCount];\r\n\r\n\t\tforeach ( var (edge, faces) in mesh.BuildEdgeFaces() )\r\n\t\t{\r\n\t\t\tif ( faces.Count == 1 )\r\n\t\t\t{\r\n\t\t\t\tboundary[edge.A] = true;\r\n\t\t\t\tboundary[edge.B] = true;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn boundary;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E1 at an opening, falling to 0 at \u003Cparamref name=\u0022reach\u0022/\u003E away from it.\u003C/summary\u003E\r\n\tpublic static float[] FlareWeights( PolyMesh mesh, bool[] boundary, float reach )\r\n\t{\r\n\t\tvar weights = new float[mesh.VertexCount];\r\n\t\tvar edge = new List\u003CVec3\u003E();\r\n\r\n\t\tfor ( var i = 0; i \u003C boundary.Length; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( boundary[i] )\r\n\t\t\t\tedge.Add( mesh.Positions[i] );\r\n\t\t}\r\n\r\n\t\tif ( edge.Count == 0 || reach \u003C= 0f )\r\n\t\t\treturn weights;\r\n\r\n\t\tfor ( var i = 0; i \u003C mesh.VertexCount; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar p = mesh.Positions[i];\r\n\t\t\tvar best = float.MaxValue;\r\n\r\n\t\t\tforeach ( var e in edge )\r\n\t\t\t\tbest = Math.Min( best, (e - p).LengthSquared );\r\n\r\n\t\t\tvar d = MathF.Sqrt( best );\r\n\t\t\tvar w = Math.Clamp( 1f - d / reach, 0f, 1f );\r\n\t\t\tweights[i] = w * w;\r\n\t\t}\r\n\r\n\t\treturn weights;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// UVs wrapped round each limb on its own bone, a foot to a UV unit everywhere.\r\n\t///\r\n\t/// ONE CYLINDER ROUND THE WHOLE BODY WAS WRONG FOR THE SLEEVES. CylinderUVs wraps everything\r\n\t/// about the body\u0027s vertical axis, which is right for the torso and stretches a sleeve into a\r\n\t/// smear: the arm is a tube lying across that projection, so its texture ran the wrong way and\r\n\t/// was squeezed to a sliver on the top and bottom of the arm. Trouser legs the same, less so.\r\n\t/// So each face is projected round the bone chain its region runs along - the torso\u0027s spine,\r\n\t/// each arm, each leg - with the angle scaled by that limb\u0027s own girth, so a check the size of\r\n\t/// a thumbnail on the chest is the size of a thumbnail on the cuff. The seam between a region\r\n\t/// and the next is a UV seam, as it is on every real garment.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static void LimbUVs( PolyMesh mesh, BodyRegions.Map map )\r\n\t{\r\n\t\tconst float UnitsPerUv = 12f;\r\n\r\n\t\t// Every face\u0027s chain, and every chain\u0027s girth from the faces on it.\r\n\t\tvar chains = new Dictionary\u003C(BodyRegion, int), (Vec3 From, Vec3 Axis, Vec3 E1, Vec3 E2, float Radius, int Count)\u003E();\r\n\t\tvar faceChain = new (BodyRegion, int)[mesh.FaceCount];\r\n\r\n\t\tfor ( var fi = 0; fi \u003C mesh.FaceCount; fi\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar centroid = mesh.FaceCentroid( mesh.Faces[fi] );\r\n\t\t\tvar (region, _) = map.Locate( centroid );\r\n\t\t\tvar side = BodyRegions.Map.IsCentral( region ) ? 0 : centroid.y \u003E 0 ? 1 : -1;\r\n\t\t\tvar key = (region, side);\r\n\t\t\tfaceChain[fi] = key;\r\n\r\n\t\t\tif ( !chains.TryGetValue( key, out var chain ) )\r\n\t\t\t{\r\n\t\t\t\tVec3 from, to;\r\n\r\n\t\t\t\tif ( region == BodyRegion.None || !map.Chain( region, side == 0 ? 1 : side, out from, out to ) )\r\n\t\t\t\t{\r\n\t\t\t\t\t// Nothing to hang it on: the old projection about the vertical.\r\n\t\t\t\t\tfrom = Vec3.Zero;\r\n\t\t\t\t\tto = new Vec3( 0f, 0f, -1f );\r\n\t\t\t\t}\r\n\r\n\t\t\t\tvar axis = (to - from).Normal;\r\n\t\t\t\tvar reference = MathF.Abs( axis.z ) \u003C 0.9f ? new Vec3( 0f, 0f, 1f ) : new Vec3( 1f, 0f, 0f );\r\n\t\t\t\tvar e1 = Vec3.Cross( reference, axis ).Normal;\r\n\t\t\t\tvar e2 = Vec3.Cross( axis, e1 );\r\n\t\t\t\tchain = (from, axis, e1, e2, 0f, 0);\r\n\t\t\t}\r\n\r\n\t\t\tvar q = centroid - chain.From;\r\n\t\t\tvar radial = q - chain.Axis * Vec3.Dot( q, chain.Axis );\r\n\t\t\tchain.Radius \u002B= radial.Length;\r\n\t\t\tchain.Count\u002B\u002B;\r\n\t\t\tchains[key] = chain;\r\n\t\t}\r\n\r\n\t\tforeach ( var fi in Enumerable.Range( 0, mesh.FaceCount ) )\r\n\t\t{\r\n\t\t\tvar face = mesh.Faces[fi];\r\n\t\t\tvar chain = chains[faceChain[fi]];\r\n\t\t\tvar radius = MathF.Max( 1f, chain.Radius / Math.Max( 1, chain.Count ) );\r\n\t\t\tvar span = MathF.PI * 2f * radius / UnitsPerUv;\r\n\t\t\tvar uvs = new Vec2[face.Count];\r\n\r\n\t\t\tfor ( var c = 0; c \u003C face.Count; c\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tvar q = mesh.Positions[face.Indices[c]] - chain.From;\r\n\t\t\t\tvar angle = MathF.Atan2( Vec3.Dot( q, chain.E2 ), Vec3.Dot( q, chain.E1 ) );\r\n\t\t\t\tuvs[c] = new Vec2( angle * radius / UnitsPerUv, Vec3.Dot( q, chain.Axis ) / UnitsPerUv );\r\n\t\t\t}\r\n\r\n\t\t\t// A face straddling the seam at \u002B-180 degrees would stretch across the whole texture.\r\n\t\t\tvar max = uvs.Max( u =\u003E u.x );\r\n\r\n\t\t\tfor ( var c = 0; c \u003C uvs.Length; c\u002B\u002B )\r\n\t\t\t\tif ( max - uvs[c].x \u003E span * 0.5f )\r\n\t\t\t\t\tuvs[c] = new Vec2( uvs[c].x \u002B span, uvs[c].y );\r\n\r\n\t\t\tface.UVs = uvs;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// A wrap-around projection about the vertical axis, a foot to a UV unit. Fabric tiles and fur\r\n\t/// noise both want something even; the body\u0027s own UVs are laid out for the body, and a CAD part\r\n\t/// may have none. LimbUVs is what Fit uses now; this is kept for a mesh with no rig to go by.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static void CylinderUVs( PolyMesh mesh )\r\n\t{\r\n\t\tconst float UnitsPerUv = 12f;\r\n\t\tvar centre = Vec3.Zero;\r\n\r\n\t\tforeach ( var p in mesh.Positions )\r\n\t\t\tcentre \u002B= p;\r\n\r\n\t\tcentre /= Math.Max( 1, mesh.VertexCount );\r\n\r\n\t\tforeach ( var face in mesh.Faces )\r\n\t\t{\r\n\t\t\tvar uvs = new Vec2[face.Count];\r\n\r\n\t\t\tfor ( var c = 0; c \u003C face.Count; c\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tvar p = mesh.Positions[face.Indices[c]];\r\n\t\t\t\tvar angle = MathF.Atan2( p.y - centre.y, p.x - centre.x );\r\n\t\t\t\tuvs[c] = new Vec2( angle / (MathF.PI * 2f) * (MathF.PI * 2f * 8f) / UnitsPerUv, -p.z / UnitsPerUv );\r\n\t\t\t}\r\n\r\n\t\t\t// A face straddling the seam at \u002B-180 degrees would stretch across the whole texture.\r\n\t\t\tvar span = MathF.PI * 2f * 8f / UnitsPerUv;\r\n\t\t\tvar max = uvs.Max( u =\u003E u.x );\r\n\r\n\t\t\tfor ( var c = 0; c \u003C uvs.Length; c\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tif ( max - uvs[c].x \u003E span * 0.5f )\r\n\t\t\t\t\tuvs[c] = new Vec2( uvs[c].x \u002B span, uvs[c].y );\r\n\t\t\t}\r\n\r\n\t\t\tface.UVs = uvs;\r\n\t\t}\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// What is wrong with a garment before you export it, said in the terms of the job.\r\n///\r\n/// WHY THIS IS NOT MeshValidator. That answers \u0022is this a mesh\u0022 \u2014 corners, indices in range,\r\n/// manifold edges \u2014 and a garment can pass every one of those checks and still be unwearable. The\r\n/// questions that decide whether a piece of clothing is finished are different ones: does it clip\r\n/// through the body when it is worn, can it be textured, will it move with the skeleton, is it\r\n/// affordable. Structure is included because it matters too, but it is one line of five.\r\n///\r\n/// IT REPORTS, IT DOES NOT REFUSE. Every number here has a legitimate reason to be non-zero. A\r\n/// garment SHOULD have openings \u2014 a T-shirt has four \u2014 and calling a neck hole an error would teach\r\n/// people to fear the thing that makes it a shirt. Clipping is the one that is nearly always a\r\n/// fault, and even that is a judgement about how deep and how much rather than a boolean. So this\r\n/// hands back counts and depths for somebody to read, and says which of them usually mean trouble.\r\n/// \u003C/summary\u003E\r\npublic sealed class GarmentReport\r\n{\r\n\tpublic string Name = \u0022\u0022;\r\n\r\n\tpublic int Faces;\r\n\tpublic int Triangles;\r\n\r\n\t/// \u003Csummary\u003EVertices sitting INSIDE a body rather than on or outside it \u2014 the garment clipping\r\n\t/// through whoever is wearing it. The number that most often means real trouble.\u003C/summary\u003E\r\n\tpublic int Clipping;\r\n\r\n\t/// \u003Csummary\u003EHow far the worst of them is in, in inches. A hundredth of an inch is the solver\r\n\t/// resting on the surface; a quarter of an inch is a shoulder through a sleeve.\u003C/summary\u003E\r\n\tpublic float DeepestClip;\r\n\r\n\t/// \u003Csummary\u003EWhere they are, one point per clipping vertex, so a viewport can mark them.\u003C/summary\u003E\r\n\tpublic List\u003CVec3\u003E ClipPoints = new();\r\n\r\n\t/// \u003Csummary\u003EHow deep each of \u003Csee cref=\u0022ClipPoints\u0022/\u003E is, in inches, in the same order.\u003C/summary\u003E\r\n\tpublic List\u003Cfloat\u003E ClipDepths = new();\r\n\r\n\t/// \u003Csummary\u003EFaces with no area \u2014 invisible, and a source of broken normals and bad lighting\r\n\t/// downstream. Always a fault.\u003C/summary\u003E\r\n\tpublic int Degenerate;\r\n\r\n\t/// \u003Csummary\u003EBoundary loops: the holes a garment is MEANT to have. A T-shirt has four \u2014 neck,\r\n\t/// hem, two cuffs. Reported so the number can be recognised, never as an error.\u003C/summary\u003E\r\n\tpublic int Openings;\r\n\r\n\tpublic bool HasUVs;\r\n\tpublic bool HasWeights;\r\n\r\n\t/// \u003Csummary\u003EStructural problems, straight from MeshValidator. These are faults.\u003C/summary\u003E\r\n\tpublic List\u003Cstring\u003E Structure = new();\r\n\r\n\t/// \u003Csummary\u003EEverything worth saying, in the order somebody reading it would want it \u2014 worst\r\n\t/// first, then the merely notable, then the reassuring.\u003C/summary\u003E\r\n\tpublic IEnumerable\u003Cstring\u003E Lines()\r\n\t{\r\n\t\tif ( Clipping \u003E 0 )\r\n\t\t\tyield return $\u0022{Clipping} {(Clipping == 1 ? \u0022vertex clips\u0022 : \u0022vertices clip\u0022)} through the body, \u0022\r\n\t\t\t\t\u002B $\u0022deepest {DeepestClip:0.##} in. Raise Looseness or Clearance, or shrinkwrap it back out.\u0022;\r\n\r\n\t\tif ( Degenerate \u003E 0 )\r\n\t\t\tyield return $\u0022{Degenerate} face{(Degenerate == 1 ? \u0022 has\u0022 : \u0022s have\u0022)} no area. \u0022\r\n\t\t\t\t\u002B \u0022Merge by distance will usually take them out.\u0022;\r\n\r\n\t\tforeach ( var problem in Structure )\r\n\t\t\tyield return problem;\r\n\r\n\t\tif ( !HasWeights )\r\n\t\t\tyield return \u0022No skin weights - it will not move with the body. Copy weights from the body you cut it from.\u0022;\r\n\r\n\t\tif ( !HasUVs )\r\n\t\t\tyield return \u0022No UVs - it cannot be textured yet.\u0022;\r\n\r\n\t\tyield return $\u0022{Triangles} triangles, {Openings} opening{(Openings == 1 ? \u0022\u0022 : \u0022s\u0022)}\u0022\r\n\t\t\t\u002B $\u0022{(HasUVs ? \u0022, UVs\u0022 : \u0022\u0022)}{(HasWeights ? \u0022, weighted\u0022 : \u0022\u0022)}.\u0022;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ENothing here is a fault. Openings and triangle count are not faults.\u003C/summary\u003E\r\n\tpublic bool Clean =\u003E Clipping == 0 \u0026\u0026 Degenerate == 0 \u0026\u0026 Structure.Count == 0 \u0026\u0026 HasUVs \u0026\u0026 HasWeights;\r\n\r\n\tpublic override string ToString() =\u003E string.Join( \u0022 \u0022, Lines() );\r\n}\r\n\r\n/// \u003Csummary\u003ECheck a garment against the bodies wearing it. See \u003Csee cref=\u0022GarmentReport\u0022/\u003E.\u003C/summary\u003E\r\npublic static class GarmentCheck\r\n{\r\n\t/// \u003Csummary\u003EHow far inside a surface counts as clipping rather than as resting on it. The\r\n\t/// drape solver settles vertices ON the body by design, and a tolerance smaller than its own\r\n\t/// resting error would report every well-fitted garment as broken.\u003C/summary\u003E\r\n\tpublic const float ClipTolerance = 0.01f;\r\n\r\n\tpublic static GarmentReport Run( PolyMesh garment, IReadOnlyList\u003CPolyMesh\u003E bodies )\r\n\t{\r\n\t\tvar report = new GarmentReport();\r\n\r\n\t\tif ( garment is null )\r\n\t\t\treturn report;\r\n\r\n\t\treport.Faces = garment.FaceCount;\r\n\t\treport.HasWeights = garment.IsRigged;\r\n\r\n\t\tforeach ( var face in garment.Faces )\r\n\t\t{\r\n\t\t\treport.Triangles \u002B= Math.Max( 0, face.Count - 2 );\r\n\r\n\t\t\tif ( face.UVs is { Length: \u003E 0 } \u0026\u0026 face.UVs.Any( uv =\u003E uv.x != 0f || uv.y != 0f ) )\r\n\t\t\t\treport.HasUVs = true;\r\n\r\n\t\t\tif ( FaceArea( garment, face ) \u003C= 1e-9f )\r\n\t\t\t\treport.Degenerate\u002B\u002B;\r\n\t\t}\r\n\r\n\t\tvar validation = MeshValidator.Validate( garment );\r\n\t\treport.Structure.AddRange( validation.Errors );\r\n\r\n\t\treport.Openings = BoundaryLoops( garment );\r\n\r\n\t\t// The same signed-distance test the fitter uses to push a garment out - see\r\n\t\t// GarmentFit.PushOut. Reusing the idiom matters: a check that disagreed with the solver\r\n\t\t// about which side of a surface a point is on would report faults the fitter cannot fix.\r\n\t\tforeach ( var body in bodies ?? Array.Empty\u003CPolyMesh\u003E() )\r\n\t\t{\r\n\t\t\tif ( body is null || body.FaceCount == 0 || ReferenceEquals( body, garment ) )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar tree = MeshBVH.Build( body );\r\n\r\n\t\t\tforeach ( var p in garment.Positions )\r\n\t\t\t{\r\n\t\t\t\tif ( tree.NearestSurface( body, p, 1e6f ) is not { } hit )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tvar depth = -Vec3.Dot( p - hit.Point, hit.Normal );\r\n\r\n\t\t\t\tif ( depth \u003C= ClipTolerance )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\treport.Clipping\u002B\u002B;\r\n\t\t\t\treport.DeepestClip = Math.Max( report.DeepestClip, depth );\r\n\t\t\t\treport.ClipPoints.Add( p );\r\n\t\t\t\treport.ClipDepths.Add( depth );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn report;\r\n\t}\r\n\r\n\tstatic float FaceArea( PolyMesh mesh, Face face )\r\n\t{\r\n\t\tif ( face.Count \u003C 3 )\r\n\t\t\treturn 0f;\r\n\r\n\t\tvar area = Vec3.Zero;\r\n\t\tvar origin = mesh.Positions[face.Indices[0]];\r\n\r\n\t\tfor ( var i = 1; i \u002B 1 \u003C face.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar a = mesh.Positions[face.Indices[i]] - origin;\r\n\t\t\tvar b = mesh.Positions[face.Indices[i \u002B 1]] - origin;\r\n\t\t\tarea \u002B= Vec3.Cross( a, b );\r\n\t\t}\r\n\r\n\t\treturn area.Length * 0.5f;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// How many separate holes the mesh has, by walking the boundary edges into loops.\r\n\t///\r\n\t/// LOOPS, NOT EDGES. MeshValidator already counts boundary edges, and that number is useless\r\n\t/// here: a T-shirt\u0027s four openings are a few hundred edges, which says nothing a person can\r\n\t/// check. Four is a number you can recognise as right.\r\n\t/// \u003C/summary\u003E\r\n\tstatic int BoundaryLoops( PolyMesh mesh )\r\n\t{\r\n\t\tvar next = new Dictionary\u003Cint, List\u003Cint\u003E\u003E();\r\n\r\n\t\tforeach ( var (key, faces) in mesh.BuildEdgeFaces() )\r\n\t\t{\r\n\t\t\tif ( faces.Count != 1 )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tif ( !next.TryGetValue( key.A, out var a ) )\r\n\t\t\t\tnext[key.A] = a = new List\u003Cint\u003E();\r\n\r\n\t\t\tif ( !next.TryGetValue( key.B, out var b ) )\r\n\t\t\t\tnext[key.B] = b = new List\u003Cint\u003E();\r\n\r\n\t\t\ta.Add( key.B );\r\n\t\t\tb.Add( key.A );\r\n\t\t}\r\n\r\n\t\tvar seen = new HashSet\u003Cint\u003E();\r\n\t\tvar loops = 0;\r\n\r\n\t\tforeach ( var start in next.Keys )\r\n\t\t{\r\n\t\t\tif ( !seen.Add( start ) )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tloops\u002B\u002B;\r\n\r\n\t\t\t// Flood the connected run of boundary vertices. A figure-of-eight boundary counts once,\r\n\t\t\t// which is the honest answer - it is one hole pinched, not two.\r\n\t\t\tvar stack = new Stack\u003Cint\u003E();\r\n\t\t\tstack.Push( start );\r\n\r\n\t\t\twhile ( stack.Count \u003E 0 )\r\n\t\t\t{\r\n\t\t\t\tforeach ( var n in next[stack.Pop()] )\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( seen.Add( n ) )\r\n\t\t\t\t\t\tstack.Push( n );\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn loops;\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003EWhere a trim goes, and what it is.\u003C/summary\u003E\r\npublic enum TrimStyle\r\n{\r\n\t/// \u003Csummary\u003EA gathered strip: longer than the edge it hangs from, so it waves. Frills, ruffles,\r\n\t/// a gathered hem.\u003C/summary\u003E\r\n\tFrill,\r\n\r\n\t/// \u003Csummary\u003EA flat band following the edge. Ribbon, binding, a cuff band, a waistband.\u003C/summary\u003E\r\n\tRibbon,\r\n\r\n\t/// \u003Csummary\u003EFolded rather than gathered - the wave is angular instead of round. Pleats, a\r\n\t/// box-pleated hem, an accordion cuff.\u003C/summary\u003E\r\n\tPleat,\r\n\r\n\t/// \u003Csummary\u003ECut into separate hanging strips. Fringe, tassels, a shredded hem.\u003C/summary\u003E\r\n\tFringe,\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Frills, ruffles, ribbons, pleats and fringe: the details that make a garment a piece of FASHION\r\n/// rather than a fitted bag.\r\n///\r\n/// THEY HANG OFF THE OPENINGS, and that is the whole idea. A garment already has exactly the edges\r\n/// trim belongs on - the hem, the collar, the cuffs - because they are its boundary loops, the holes\r\n/// it is made to have. Nothing has to be selected, drawn or marked: the shape of the garment already\r\n/// says where a frill could go, so asking for one is choosing which opening and how much.\r\n///\r\n/// LACE IS NOT HERE, DELIBERATELY. Lace is holes in cloth, and holes in cloth are an alpha-masked\r\n/// material - that is how every shipped game does it, because modelling the mesh of a lace edging\r\n/// costs thousands of triangles to render something a 512px texture renders better. Put lace on the\r\n/// garment as a material with a cut-out; use a Frill for the shape the lace edging follows.\r\n///\r\n/// GATHERED, NOT SCALED. A real frill is a strip of cloth LONGER than the edge it is sewn to, forced\r\n/// to fit by being bunched. That is why it waves, and why the waves get deeper further from the seam\r\n/// while the seam itself stays smooth. So this displaces along the surface normal by an amount that\r\n/// grows with the distance hung, rather than making a wavy ring and pulling it down - which looks\r\n/// like a cake decoration and not like cloth.\r\n/// \u003C/summary\u003E\r\npublic static class GarmentTrim\r\n{\r\n\tpublic sealed class Options\r\n\t{\r\n\t\tpublic TrimStyle Style = TrimStyle.Frill;\r\n\r\n\t\t/// \u003Csummary\u003EHow far the trim hangs from the edge, in inches.\u003C/summary\u003E\r\n\t\tpublic float Width = 1.2f;\r\n\r\n\t\t/// \u003Csummary\u003EHow much longer than its edge the strip is, 0 flat to 1 heavily bunched. What\r\n\t\t/// makes a frill a frill.\u003C/summary\u003E\r\n\t\tpublic float Gather = 0.5f;\r\n\r\n\t\t/// \u003Csummary\u003EHow many waves around the whole opening. More waves, finer gathers.\u003C/summary\u003E\r\n\t\tpublic int Waves = 24;\r\n\r\n\t\t/// \u003Csummary\u003ERows of quads from the seam to the free edge. One is enough for a ribbon; a deep\r\n\t\t/// frill wants four or five so the wave has somewhere to develop.\u003C/summary\u003E\r\n\t\tpublic int Rows = 3;\r\n\r\n\t\t/// \u003Csummary\u003EFor Fringe: what fraction of each strip\u0027s slot is cut away.\u003C/summary\u003E\r\n\t\tpublic float Gap = 0.4f;\r\n\r\n\t\tpublic int MaterialSlot = 0;\r\n\t}\r\n\r\n\tpublic sealed class Result\r\n\t{\r\n\t\tpublic PolyMesh Mesh = new();\r\n\t\tpublic int Openings;\r\n\t\tpublic string Problem;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Build trim along some of a mesh\u0027s boundary loops.\r\n\t///\r\n\t/// \u003Cparamref name=\u0022which\u0022/\u003E picks loops by index into the order OrderedBoundaryLoops returns\r\n\t/// them in, which is by height, lowest first - so on a shirt 0 is the hem and the last is the\r\n\t/// collar. Null or empty means every opening.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static Result Build( PolyMesh garment, IReadOnlyList\u003Cint\u003E which, Options o )\r\n\t{\r\n\t\tvar result = new Result();\r\n\r\n\t\tif ( garment is null || garment.FaceCount == 0 )\r\n\t\t{\r\n\t\t\tresult.Problem = \u0022there is no garment to trim\u0022;\r\n\t\t\treturn result;\r\n\t\t}\r\n\r\n\t\tvar loops = OrderedBoundaryLoops( garment );\r\n\r\n\t\tif ( loops.Count == 0 )\r\n\t\t{\r\n\t\t\tresult.Problem = \u0022this garment has no openings - trim hangs from an edge, and a closed \u0022\r\n\t\t\t\t\u002B \u0022shape has none\u0022;\r\n\t\t\treturn result;\r\n\t\t}\r\n\r\n\t\tvar normals = garment.ComputeVertexNormals();\r\n\t\tvar rows = Math.Clamp( o.Rows, 1, 12 );\r\n\r\n\t\tfor ( var li = 0; li \u003C loops.Count; li\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( which is { Count: \u003E 0 } \u0026\u0026 !which.Contains( li ) )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tif ( loops[li].Count \u003C 3 )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tAddStrip( result.Mesh, garment, loops[li], normals, rows, o );\r\n\t\t\tresult.Openings\u002B\u002B;\r\n\t\t}\r\n\r\n\t\tif ( result.Openings == 0 )\r\n\t\t\tresult.Problem = $\u0022none of this garment\u0027s {loops.Count} openings were chosen\u0022;\r\n\r\n\t\treturn result;\r\n\t}\r\n\r\n\tstatic void AddStrip( PolyMesh into, PolyMesh garment, List\u003Cint\u003E ring, Vec3[] normals, int rows,\r\n\t\tOptions o )\r\n\t{\r\n\t\tvar n = ring.Count;\r\n\r\n\t\t// Arc length around the ring, so the wave count means the same thing whether the ring is a\r\n\t\t// cuff or a hem and whether its vertices are evenly spaced or not. Spacing waves by INDEX\r\n\t\t// instead puts every wave of a hem into the densest quarter of it.\r\n\t\tvar arc = new float[n \u002B 1];\r\n\r\n\t\tfor ( var i = 0; i \u003C n; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar a = garment.Positions[ring[i]];\r\n\t\t\tvar b = garment.Positions[ring[(i \u002B 1) % n]];\r\n\t\t\tarc[i \u002B 1] = arc[i] \u002B (b - a).Length;\r\n\t\t}\r\n\r\n\t\tvar total = arc[n];\r\n\r\n\t\tif ( total \u003C= 1e-5f )\r\n\t\t\treturn;\r\n\r\n\t\tvar centre = Vec3.Zero;\r\n\r\n\t\tforeach ( var v in ring )\r\n\t\t\tcentre \u002B= garment.Positions[v];\r\n\r\n\t\tcentre /= n;\r\n\r\n\t\t// Which way the trim hangs: the direction the cloth would carry on in if the garment did not\r\n\t\t// stop at its own hem. Taken from the edge rather than as \u0022straight down\u0022, so a cuff\u0027s frill\r\n\t\t// runs on down the arm and a collar\u0027s stands off the neck.\r\n\t\tvar away = new Vec3[n];\r\n\r\n\t\tfor ( var i = 0; i \u003C n; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar p = garment.Positions[ring[i]];\r\n\t\t\tvar prev = garment.Positions[ring[(i - 1 \u002B n) % n]];\r\n\t\t\tvar next = garment.Positions[ring[(i \u002B 1) % n]];\r\n\r\n\t\t\tvar tangent = (next - prev).Normal;\r\n\t\t\tvar outward = Vec3.Cross( tangent, normals[ring[i]] );\r\n\r\n\t\t\tif ( outward.LengthSquared \u003C 1e-8f )\r\n\t\t\t\toutward = p - centre;\r\n\r\n\t\t\toutward = outward.Normal;\r\n\r\n\t\t\t// The cross product\u0027s sign flips with the ring\u0027s winding, which is not something a\r\n\t\t\t// caller should have to know about. Point it away from the ring\u0027s own centre.\r\n\t\t\tif ( Vec3.Dot( outward, p - centre ) \u003C 0f )\r\n\t\t\t\toutward = -outward;\r\n\r\n\t\t\taway[i] = outward;\r\n\t\t}\r\n\r\n\t\tvar waves = Math.Max( 1, o.Waves );\r\n\t\tvar gather = Math.Clamp( o.Gather, 0f, 1f );\r\n\t\tvar width = Math.Max( 0f, o.Width );\r\n\r\n\t\t// Row 0 is the seam: it sits exactly on the garment\u0027s own edge, unmoved, so the trim is\r\n\t\t// attached rather than floating near it.\r\n\t\tvar grid = new int[rows \u002B 1][];\r\n\r\n\t\tfor ( var r = 0; r \u003C= rows; r\u002B\u002B )\r\n\t\t{\r\n\t\t\tgrid[r] = new int[n];\r\n\r\n\t\t\tvar t = r / (float)rows;\r\n\r\n\t\t\tfor ( var i = 0; i \u003C n; i\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tvar p = garment.Positions[ring[i]];\r\n\t\t\t\tvar s = arc[i] / total;\r\n\r\n\t\t\t\tvar swing = o.Style switch\r\n\t\t\t\t{\r\n\t\t\t\t\tTrimStyle.Frill =\u003E MathF.Sin( s * MathF.Tau * waves ),\r\n\t\t\t\t\tTrimStyle.Pleat =\u003E Triangle( s * waves ),\r\n\t\t\t\t\t_ =\u003E 0f,\r\n\t\t\t\t};\r\n\r\n\t\t\t\t// The gather grows with distance from the seam, which is what keeps the seam itself\r\n\t\t\t\t// from rippling. A ribbon has no gather at all and stays a flat band.\r\n\t\t\t\tvar offset = away[i] * (width * t)\r\n\t\t\t\t\t\u002B normals[ring[i]] * (swing * gather * width * 0.6f * t);\r\n\r\n\t\t\t\tgrid[r][i] = into.Positions.Count;\r\n\t\t\t\tinto.Positions.Add( p \u002B offset );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tvar fringe = o.Style == TrimStyle.Fringe;\r\n\t\tvar keep = Math.Clamp( 1f - o.Gap, 0.05f, 1f );\r\n\r\n\t\tfor ( var r = 0; r \u003C rows; r\u002B\u002B )\r\n\t\t{\r\n\t\t\tfor ( var i = 0; i \u003C n; i\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tvar j = (i \u002B 1) % n;\r\n\r\n\t\t\t\t// Fringe is the same strip with most of it cut away: whole slots are dropped rather\r\n\t\t\t\t// than narrowed, so what is left hangs as separate tassels and not as a comb.\r\n\t\t\t\tif ( fringe \u0026\u0026 r \u003E 0 )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar phase = arc[i] / total * waves;\r\n\r\n\t\t\t\t\tif ( phase - MathF.Floor( phase ) \u003E keep )\r\n\t\t\t\t\t\tcontinue;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tinto.Faces.Add( new Face( new[] { grid[r][i], grid[r][j], grid[r \u002B 1][j], grid[r \u002B 1][i] } )\r\n\t\t\t\t{\r\n\t\t\t\t\tMaterial = o.MaterialSlot,\r\n\t\t\t\t} );\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EA 0..1 sawtooth as a -1..1 triangle wave: a pleat\u0027s fold, angular where a frill\u0027s\r\n\t/// is round.\u003C/summary\u003E\r\n\tstatic float Triangle( float x )\r\n\t{\r\n\t\tvar f = x - MathF.Floor( x );\r\n\t\treturn f \u003C 0.5f ? f * 4f - 1f : 3f - f * 4f;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Every boundary loop as an ORDERED ring of vertices, lowest first.\r\n\t///\r\n\t/// ORDERED IS THE WHOLE JOB. GarmentCheck only has to COUNT openings, so it floods them; trim\r\n\t/// has to walk one in sequence to lay quads along it. Height ordering is what makes \u0022opening 0\u0022\r\n\t/// mean the hem on every garment rather than whichever vertex happened to be found first.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static List\u003CList\u003Cint\u003E\u003E OrderedBoundaryLoops( PolyMesh mesh )\r\n\t{\r\n\t\tvar neighbours = new Dictionary\u003Cint, List\u003Cint\u003E\u003E();\r\n\r\n\t\tforeach ( var (key, faces) in mesh.BuildEdgeFaces() )\r\n\t\t{\r\n\t\t\tif ( faces.Count != 1 )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tif ( !neighbours.TryGetValue( key.A, out var a ) )\r\n\t\t\t\tneighbours[key.A] = a = new List\u003Cint\u003E();\r\n\r\n\t\t\tif ( !neighbours.TryGetValue( key.B, out var b ) )\r\n\t\t\t\tneighbours[key.B] = b = new List\u003Cint\u003E();\r\n\r\n\t\t\ta.Add( key.B );\r\n\t\t\tb.Add( key.A );\r\n\t\t}\r\n\r\n\t\tvar loops = new List\u003CList\u003Cint\u003E\u003E();\r\n\t\tvar used = new HashSet\u003Cint\u003E();\r\n\r\n\t\tforeach ( var start in neighbours.Keys )\r\n\t\t{\r\n\t\t\tif ( used.Contains( start ) )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar loop = new List\u003Cint\u003E();\r\n\t\t\tvar current = start;\r\n\t\t\tvar previous = -1;\r\n\r\n\t\t\t// Walk the ring. A boundary vertex normally has exactly two boundary neighbours; where\r\n\t\t\t// it has more the loop is pinched, and taking the first unused one keeps the walk going\r\n\t\t\t// rather than abandoning the opening.\r\n\t\t\twhile ( current \u003E= 0 \u0026\u0026 used.Add( current ) )\r\n\t\t\t{\r\n\t\t\t\tloop.Add( current );\r\n\r\n\t\t\t\tvar next = -1;\r\n\r\n\t\t\t\tforeach ( var candidate in neighbours[current] )\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( candidate == previous || used.Contains( candidate ) )\r\n\t\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\t\tnext = candidate;\r\n\t\t\t\t\tbreak;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tprevious = current;\r\n\t\t\t\tcurrent = next;\r\n\t\t\t}\r\n\r\n\t\t\tif ( loop.Count \u003E= 3 )\r\n\t\t\t\tloops.Add( loop );\r\n\t\t}\r\n\r\n\t\tloops.Sort( ( a, b ) =\u003E Height( mesh, a ).CompareTo( Height( mesh, b ) ) );\r\n\r\n\t\treturn loops;\r\n\t}\r\n\r\n\tstatic float Height( PolyMesh mesh, List\u003Cint\u003E loop )\r\n\t{\r\n\t\tvar z = 0f;\r\n\r\n\t\tforeach ( var v in loop )\r\n\t\t\tz \u002B= mesh.Positions[v].z;\r\n\r\n\t\treturn z / Math.Max( 1, loop.Count );\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Turn a garment into an s\u0026amp;box \u0060.clothing\u0060 definition: the record that makes a mesh into a\r\n/// wearable item rather than a prop.\r\n///\r\n/// THE STEP THIS TOOL STOPPED ONE SHORT OF. Effigy could already compile a garment to a skinned\r\n/// \u0060.vmdl\u0060, and a \u0060.vmdl\u0060 is not clothing. s\u0026amp;box dresses a citizen from a Clothing resource,\r\n/// which names the model and then answers three questions the mesh cannot: what KIND of garment it\r\n/// is, which body slots it OCCUPIES so two hats do not both get worn, and which parts of the body it\r\n/// HIDES so the chest does not poke through the shirt. Without it a finished garment cannot be put\r\n/// on a character at all, and the modelling was the easy part of making clothes.\r\n///\r\n/// THE SLOTS COME FROM THE RECIPE, not from the garment\u0027s name. The recipe already says exactly\r\n/// which regions of the body it covers - that is what it IS - so a long sleeve claims the wrists and\r\n/// a T-shirt does not, and adjusting Sleeve moves the answer with it. Deriving them from the name\r\n/// would be a second, parallel description of the same garment, and the two would drift apart the\r\n/// first time somebody dragged a slider.\r\n///\r\n/// WRITTEN AS TEXT, IN THE KERNEL. A Clothing resource is JSON, so it needs no engine to produce and\r\n/// can be tested headlessly - which matters more here than usual, because the failure mode of a\r\n/// wrong slot name is an asset that compiles, loads, and silently does not work.\r\n/// \u003C/summary\u003E\r\npublic static class ClothingDefinition\r\n{\r\n\t/// \u003Csummary\u003EWhat a garment covers, in s\u0026amp;box\u0027s own slot vocabulary. Anything not listed maps\r\n\t/// to nothing, which is the right answer for Neck.\u003C/summary\u003E\r\n\tpublic static IEnumerable\u003Cstring\u003E SlotsFor( GarmentRecipe recipe )\r\n\t{\r\n\t\tvar slots = new List\u003Cstring\u003E();\r\n\r\n\t\tvoid Add( string slot )\r\n\t\t{\r\n\t\t\tif ( !slots.Contains( slot ) )\r\n\t\t\t\tslots.Add( slot );\r\n\t\t}\r\n\r\n\t\t// A span tagged for one side claims only that side\u0027s slots. That is the whole reason the\r\n\t\t// side is on the span: a one-sleeved top that claimed both arms would stop you wearing\r\n\t\t// anything on the bare one.\r\n\t\tvoid Sided( int side, string left, string right )\r\n\t\t{\r\n\t\t\tif ( side \u003E= 0 )\r\n\t\t\t\tAdd( left );\r\n\r\n\t\t\tif ( side \u003C= 0 )\r\n\t\t\t\tAdd( right );\r\n\t\t}\r\n\r\n\t\tforeach ( var (region, from, to, side) in recipe.Spans )\r\n\t\t{\r\n\t\t\tswitch ( region )\r\n\t\t\t{\r\n\t\t\t\tcase BodyRegion.Torso:\r\n\t\t\t\t\tAdd( \u0022Chest\u0022 );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase BodyRegion.Hips:\r\n\t\t\t\t\tAdd( \u0022Groin\u0022 );\r\n\t\t\t\t\tAdd( \u0022Waist\u0022 );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase BodyRegion.UpperArm:\r\n\t\t\t\t\tSided( side, \u0022LeftArm\u0022, \u0022RightArm\u0022 );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase BodyRegion.LowerArm:\r\n\t\t\t\t\tSided( side, \u0022LeftArm\u0022, \u0022RightArm\u0022 );\r\n\r\n\t\t\t\t\t// Only a sleeve that actually reaches the wrist claims it. A three-quarter\r\n\t\t\t\t\t// sleeve stopping at the forearm must leave the wrist free for a watch.\r\n\t\t\t\t\tif ( to \u003E 0.9f )\r\n\t\t\t\t\t\tSided( side, \u0022LeftWrist\u0022, \u0022RightWrist\u0022 );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase BodyRegion.Hand:\r\n\t\t\t\t\tSided( side, \u0022LeftHand\u0022, \u0022RightHand\u0022 );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase BodyRegion.UpperLeg:\r\n\t\t\t\t\tSided( side, \u0022LeftThigh\u0022, \u0022RightThigh\u0022 );\r\n\r\n\t\t\t\t\tif ( to \u003E 0.9f )\r\n\t\t\t\t\t\tSided( side, \u0022LeftKnee\u0022, \u0022RightKnee\u0022 );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase BodyRegion.LowerLeg:\r\n\t\t\t\t\tSided( side, \u0022LeftKnee\u0022, \u0022RightKnee\u0022 );\r\n\t\t\t\t\tSided( side, \u0022LeftShin\u0022, \u0022RightShin\u0022 );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase BodyRegion.Foot:\r\n\t\t\t\t\tSided( side, \u0022LeftFoot\u0022, \u0022RightFoot\u0022 );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase BodyRegion.Head:\r\n\t\t\t\t\tAdd( \u0022HeadTop\u0022 );\r\n\r\n\t\t\t\t\t// A hat that comes down past the ears covers the lower head too - the\r\n\t\t\t\t\t// difference between a cap and a balaclava, and what stops both being wearable\r\n\t\t\t\t\t// at once.\r\n\t\t\t\t\tif ( from \u003C 0.35f )\r\n\t\t\t\t\t\tAdd( \u0022HeadBottom\u0022 );\r\n\t\t\t\t\tbreak;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn slots;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Which parts of the BODY to stop drawing underneath, in s\u0026amp;box\u0027s BodyGroups vocabulary.\r\n\t///\r\n\t/// NOT THE SAME QUESTION AS THE SLOTS, and conflating them is how clothing ends up with skin\r\n\t/// poking through it. Slots are about what else can be worn; this is about the body being drawn\r\n\t/// inside the cloth. It is deliberately conservative - only whole groups the garment substantially\r\n\t/// covers - because hiding a limb a garment only half covers leaves a character with no forearm.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static IEnumerable\u003Cstring\u003E HideBodyFor( GarmentRecipe recipe )\r\n\t{\r\n\t\tvar hide = new List\u003Cstring\u003E();\r\n\r\n\t\tvoid Add( string group )\r\n\t\t{\r\n\t\t\tif ( !hide.Contains( group ) )\r\n\t\t\t\thide.Add( group );\r\n\t\t}\r\n\r\n\t\tforeach ( var (region, from, to, _) in recipe.Spans )\r\n\t\t{\r\n\t\t\tswitch ( region )\r\n\t\t\t{\r\n\t\t\t\tcase BodyRegion.Torso when to \u003E 0.9f:\r\n\t\t\t\t\tAdd( \u0022Chest\u0022 );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\t// Legs come off only for something that reaches the ankle. Shorts hide nothing:\r\n\t\t\t\t// the leg below them is the leg you are meant to see.\r\n\t\t\t\tcase BodyRegion.LowerLeg when to \u003E 0.9f:\r\n\t\t\t\t\tAdd( \u0022Legs\u0022 );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase BodyRegion.Hand:\r\n\t\t\t\t\tAdd( \u0022Hands\u0022 );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase BodyRegion.Foot:\r\n\t\t\t\t\tAdd( \u0022Feet\u0022 );\r\n\t\t\t\t\tbreak;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn hide;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe s\u0026amp;box ClothingCategory a recipe belongs in. Named from the enum this engine\r\n\t/// actually has \u2014 see the effigy_clothing_probe console command.\u003C/summary\u003E\r\n\tpublic static string CategoryFor( GarmentRecipe recipe ) =\u003E recipe?.Name switch\r\n\t{\r\n\t\t\u0022T-shirt\u0022 =\u003E \u0022TShirt\u0022,\r\n\t\t\u0022Long sleeve\u0022 =\u003E \u0022Shirt\u0022,\r\n\t\t\u0022Tank top\u0022 =\u003E \u0022Vest\u0022,\r\n\t\t\u0022Trousers\u0022 =\u003E \u0022Trousers\u0022,\r\n\t\t\u0022Shorts\u0022 =\u003E \u0022Shorts\u0022,\r\n\t\t\u0022Beanie\u0022 =\u003E \u0022HatBeanie\u0022,\r\n\t\t\u0022Gloves\u0022 =\u003E \u0022Gloves\u0022,\r\n\t\t\u0022Socks\u0022 =\u003E \u0022Socks\u0022,\r\n\t\t\u0022Bodysuit\u0022 =\u003E \u0022Fullbody\u0022,\r\n\t\t_ =\u003E \u0022Tops\u0022,\r\n\t};\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The \u0060.clothing\u0060 file\u0027s text.\r\n\t///\r\n\t/// Only the fields that mean something for a garment made here are written. The rest - human\r\n\t/// skin overrides, alternate male and female models, tint gradients, a Steam item id - are\r\n\t/// s\u0026amp;box\u0027s own defaults, and writing guesses at them would be claiming a garment supports\r\n\t/// things it has never been tested with.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022modelPath\u0022\u003EThe compiled model, as an asset path: models/effigy/shirt.vmdl.\u003C/param\u003E\r\n\tpublic static string Write( GarmentRecipe recipe, string modelPath, string title,\r\n\t\tstring subtitle = null )\r\n\t{\r\n\t\tvar slots = string.Join( \u0022, \u0022, SlotsFor( recipe ) );\r\n\t\tvar hide = string.Join( \u0022, \u0022, HideBodyFor( recipe ) );\r\n\r\n\t\tvar sb = new StringBuilder();\r\n\r\n\t\tsb.Append( \u0022{\\n\u0022 );\r\n\t\tsb.Append( $\u0022  \\\u0022Title\\\u0022: {Json( title )},\\n\u0022 );\r\n\t\tsb.Append( $\u0022  \\\u0022Subtitle\\\u0022: {Json( subtitle ?? \u0022\u0022 )},\\n\u0022 );\r\n\t\tsb.Append( $\u0022  \\\u0022Category\\\u0022: {Json( CategoryFor( recipe ) )},\\n\u0022 );\r\n\t\tsb.Append( $\u0022  \\\u0022Model\\\u0022: {Json( modelPath )},\\n\u0022 );\r\n\r\n\t\t// An empty flags enum serialises as 0, not as \u0022\u0022. s\u0026box writes it that way itself - see any\r\n\t\t// shipped .clothing with no SlotsOver - and a \u0022\u0022 here is read as an unknown flag name.\r\n\t\tsb.Append( $\u0022  \\\u0022SlotsUnder\\\u0022: {(slots.Length \u003E 0 ? Json( slots ) : \u00220\u0022)},\\n\u0022 );\r\n\t\tsb.Append( \u0022  \\\u0022SlotsOver\\\u0022: 0,\\n\u0022 );\r\n\t\tsb.Append( $\u0022  \\\u0022HideBody\\\u0022: {(hide.Length \u003E 0 ? Json( hide ) : \u00220\u0022)},\\n\u0022 );\r\n\t\tsb.Append( \u0022  \\\u0022__references\\\u0022: [],\\n\u0022 );\r\n\t\tsb.Append( \u0022  \\\u0022__version\\\u0022: 0\\n\u0022 );\r\n\t\tsb.Append( \u0022}\\n\u0022 );\r\n\r\n\t\treturn sb.ToString();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Several garments as the one recipe their shared model wears.\r\n\t///\r\n\t/// ONE MODEL, ONE DEFINITION. Compile .vmdl bakes every garment in the document into a single\r\n\t/// model, so a shirt and trousers made together are one item and must claim both sets of slots -\r\n\t/// a .clothing that only named the shirt\u0027s would let a second pair of trousers go on over the\r\n\t/// first. The category is the first garment\u0027s: s\u0026amp;box has exactly one per item, and the first\r\n\t/// thing added is the best guess at what the item is.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static GarmentRecipe Combine( IEnumerable\u003CGarmentRecipe\u003E recipes )\r\n\t{\r\n\t\tGarmentRecipe merged = null;\r\n\r\n\t\tforeach ( var recipe in recipes )\r\n\t\t{\r\n\t\t\tif ( recipe is null )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tmerged ??= new GarmentRecipe { Name = recipe.Name };\r\n\t\t\tmerged.Spans.AddRange( recipe.Spans );\r\n\t\t}\r\n\r\n\t\treturn merged;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EA JSON string literal. Small by design - this writes four known fields, and pulling\r\n\t/// in a serialiser for them would put an engine dependency in the kernel.\u003C/summary\u003E\r\n\tstatic string Json( string value )\r\n\t{\r\n\t\tif ( value is null )\r\n\t\t\treturn \u0022null\u0022;\r\n\r\n\t\tvar sb = new StringBuilder( value.Length \u002B 2 );\r\n\t\tsb.Append( \u0027\u0022\u0027 );\r\n\r\n\t\tforeach ( var c in value )\r\n\t\t{\r\n\t\t\tswitch ( c )\r\n\t\t\t{\r\n\t\t\t\tcase \u0027\u0022\u0027: sb.Append( \u0022\\\\\\\u0022\u0022 ); break;\r\n\t\t\t\tcase \u0027\\\\\u0027: sb.Append( \u0022\\\\\\\\\u0022 ); break;\r\n\t\t\t\tcase \u0027\\n\u0027: sb.Append( \u0022\\\\n\u0022 ); break;\r\n\t\t\t\tcase \u0027\\r\u0027: sb.Append( \u0022\\\\r\u0022 ); break;\r\n\t\t\t\tcase \u0027\\t\u0027: sb.Append( \u0022\\\\t\u0022 ); break;\r\n\t\t\t\tdefault:\r\n\t\t\t\t\tif ( c \u003C 0x20 )\r\n\t\t\t\t\t\tsb.Append( \u0022\\\\u\u0022 ).Append( ((int)c).ToString( \u0022x4\u0022 ) );\r\n\t\t\t\t\telse\r\n\t\t\t\t\t\tsb.Append( c );\r\n\t\t\t\t\tbreak;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tsb.Append( \u0027\u0022\u0027 );\r\n\t\treturn sb.ToString();\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// A smooth stand-in body to cut a garment from, for a wearer that is not a skin.\r\n///\r\n/// WHY A GARMENT NEEDS ONE. GarmentFit lifts the cloth straight off the wearer\u0027s own surface,\r\n/// which is exactly right for a skin like Citizen\u0027s and exactly wrong for a body built from\r\n/// parts. Camhead is struts, plates and cables with air between them; lifted off that, a\r\n/// T-shirt came out as a tight shell over every bolt, and the drape had nothing to hang across\r\n/// the gaps. A shirt on a robot is a shirt on the robot\u0027s SILHOUETTE.\r\n///\r\n/// ONE CONVEX HULL PER REGION AND SIDE - the torso, the hips, each upper arm, each forearm - out\r\n/// of the vertices nearest that region\u0027s bones. Convex, so it spans every gap; per region, so\r\n/// the armpit and the crotch stay open instead of the whole body becoming one blob. The hulls\r\n/// overlap at the joints, and Fit already drops faces buried inside another closed body, which\r\n/// is what turns overlapping hulls into one clean outer surface.\r\n///\r\n/// SUBDIVIDED LINEARLY, not smoothed. Catmull-Clark would round the hull inward and let the\r\n/// corners of the real parts poke through the cloth; midpoint splits keep the shape and only add\r\n/// the vertices the drape needs to have something to bend.\r\n/// \u003C/summary\u003E\r\npublic static class GarmentEnvelope\r\n{\r\n\t/// \u003Csummary\u003EHow many separate pieces a mesh is made of. A skin is one or a handful (a head, a\r\n\t/// body, two eyes); a kitbashed robot is hundreds.\u003C/summary\u003E\r\n\tpublic static int ShellCount( PolyMesh mesh )\r\n\t{\r\n\t\tvar parent = new int[mesh.VertexCount];\r\n\r\n\t\tfor ( var i = 0; i \u003C parent.Length; i\u002B\u002B )\r\n\t\t\tparent[i] = i;\r\n\r\n\t\tint Find( int x )\r\n\t\t{\r\n\t\t\twhile ( parent[x] != x )\r\n\t\t\t\tx = parent[x] = parent[parent[x]];\r\n\r\n\t\t\treturn x;\r\n\t\t}\r\n\r\n\t\tforeach ( var face in mesh.Faces )\r\n\t\t\tfor ( var c = 1; c \u003C face.Count; c\u002B\u002B )\r\n\t\t\t\tparent[Find( face.Indices[c] )] = Find( face.Indices[0] );\r\n\r\n\t\tvar roots = new HashSet\u003Cint\u003E();\r\n\r\n\t\tforeach ( var face in mesh.Faces )\r\n\t\t\tif ( face.Count \u003E 0 )\r\n\t\t\t\troots.Add( Find( face.Indices[0] ) );\r\n\r\n\t\treturn roots.Count;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EShould a garment be cut from an envelope instead of the surface? More than this\r\n\t/// many pieces is a body built from parts. Citizen is under ten.\u003C/summary\u003E\r\n\tpublic const int PartsThreshold = 24;\r\n\r\n\tpublic static bool IsKitbashed( IEnumerable\u003CPolyMesh\u003E bodies ) =\u003E\r\n\t\tbodies.Sum( ShellCount ) \u003E PartsThreshold;\r\n\r\n\t/// \u003Csummary\u003EThe stand-in: one closed, subdivided hull per body region and side.\u003C/summary\u003E\r\n\tpublic static List\u003C(BodyRegion Region, PolyMesh Mesh)\u003E Build( IReadOnlyList\u003CPolyMesh\u003E bodies, BodyRegions.Map map, float edge = 1.5f )\r\n\t{\r\n\t\tvar groups = new Dictionary\u003C(BodyRegion, int), List\u003CVec3\u003E\u003E();\r\n\r\n\t\tforeach ( var body in bodies )\r\n\t\t{\r\n\t\t\tforeach ( var p in body.Positions )\r\n\t\t\t{\r\n\t\t\t\tvar (region, _) = map.Locate( p );\r\n\r\n\t\t\t\tif ( region == BodyRegion.None )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tvar central = region is BodyRegion.Head or BodyRegion.Neck or BodyRegion.Torso or BodyRegion.Hips;\r\n\t\t\t\tvar key = (region, central ? 0 : p.y \u003E 0 ? 1 : -1);\r\n\r\n\t\t\t\tif ( !groups.TryGetValue( key, out var list ) )\r\n\t\t\t\t\tgroups[key] = list = new List\u003CVec3\u003E();\r\n\r\n\t\t\t\tlist.Add( p );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// THE TORSO HANGS. Cloth falls from the widest thing it rests on - shoulders, chest - and\r\n\t\t// does not follow the body back in at the waist, which is the whole difference between a\r\n\t\t// shirt and a wetsuit. So the torso\u0027s hull is dropped straight down to the bottom of the\r\n\t\t// hips: a column as wide as the chest, and the drape has something to hang from rather\r\n\t\t// than a waist to crumple into.\r\n\t\tif ( groups.TryGetValue( (BodyRegion.Torso, 0), out var torso ) \u0026\u0026 groups.TryGetValue( (BodyRegion.Hips, 0), out var hips ) )\r\n\t\t{\r\n\t\t\tvar floor = hips.Min( p =\u003E p.z );\r\n\r\n\t\t\t// Only the chest between the arms. Dropped from the shoulder points too, the column is\r\n\t\t\t// as wide as the shoulders all the way down, swallows an A-posed upper arm, and the\r\n\t\t\t// sleeves - the faces of the arm hulls inside it - are cut away with it.\r\n\t\t\tvar inner = groups.Where( g =\u003E g.Key.Item1 == BodyRegion.UpperArm )\r\n\t\t\t\t.SelectMany( g =\u003E g.Value ).Select( p =\u003E MathF.Abs( p.y ) ).DefaultIfEmpty( float.MaxValue ).Min();\r\n\t\t\tvar dropped = torso.Where( p =\u003E MathF.Abs( p.y ) \u003C inner )\r\n\t\t\t\t.Select( p =\u003E new Vec3( p.x, p.y, floor ) ).ToList();\r\n\r\n\t\t\ttorso.AddRange( dropped );\r\n\t\t}\r\n\r\n\t\tvar hulls = new List\u003C(BodyRegion, PolyMesh)\u003E();\r\n\r\n\t\tforeach ( var ((groupRegion, _), points) in groups )\r\n\t\t{\r\n\t\t\tif ( points.Count \u003C 4 )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tPolyMesh hull;\r\n\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\thull = ConvexHull.ToMesh( Extremes( points ), 1e-4f );\r\n\t\t\t}\r\n\t\t\tcatch ( Exception )\r\n\t\t\t{\r\n\t\t\t\t// Flat or degenerate - a single plate on its own. Nothing to wrap.\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif ( hull is null || hull.FaceCount == 0 )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\thulls.Add( (groupRegion, Refine( hull, edge )) );\r\n\t\t}\r\n\r\n\t\treturn hulls;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The points furthest along a few hundred directions spread evenly over the sphere.\r\n\t///\r\n\t/// ConvexHull is incremental and sized for CAD bodies of a few hundred vertices; a robot\u0027s\r\n\t/// torso is thousands, nearly all of them inside, and hulling them all took minutes. The\r\n\t/// extremes are the hull\u0027s own corners to within a fraction of an inch, which is far closer\r\n\t/// than cloth sitting a clearance off it can tell apart.\r\n\t/// \u003C/summary\u003E\r\n\tstatic List\u003CVec3\u003E Extremes( List\u003CVec3\u003E points, int directions = 320 )\r\n\t{\r\n\t\tif ( points.Count \u003C= directions )\r\n\t\t\treturn points;\r\n\r\n\t\tvar keep = new HashSet\u003Cint\u003E();\r\n\t\tvar golden = MathF.PI * (3f - MathF.Sqrt( 5f ));\r\n\r\n\t\tfor ( var d = 0; d \u003C directions; d\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar z = 1f - 2f * (d \u002B 0.5f) / directions;\r\n\t\t\tvar r = MathF.Sqrt( MathF.Max( 0f, 1f - z * z ) );\r\n\t\t\tvar dir = new Vec3( r * MathF.Cos( golden * d ), r * MathF.Sin( golden * d ), z );\r\n\r\n\t\t\tvar best = 0;\r\n\t\t\tvar bestDot = float.MinValue;\r\n\r\n\t\t\tfor ( var i = 0; i \u003C points.Count; i\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tvar dot = Vec3.Dot( points[i], dir );\r\n\r\n\t\t\t\tif ( dot \u003E bestDot )\r\n\t\t\t\t{\r\n\t\t\t\t\tbestDot = dot;\r\n\t\t\t\t\tbest = i;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tkeep.Add( best );\r\n\t\t}\r\n\r\n\t\treturn keep.Select( i =\u003E points[i] ).ToList();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ETriangulate, then split every triangle four ways until the longest edge is under\r\n\t/// \u003Cparamref name=\u0022edge\u0022/\u003E. Uniform on purpose: every triangle splits every round, so shared\r\n\t/// edges always split together and the hull stays closed - a split that followed each edge\u0027s\r\n\t/// own length would leave T-junctions wherever a long face met a short one.\u003C/summary\u003E\r\n\tpublic static PolyMesh Refine( PolyMesh mesh, float edge, int maxTriangles = 12000 )\r\n\t{\r\n\t\tvar positions = new List\u003CVec3\u003E( mesh.Positions );\r\n\t\tvar tris = new List\u003C(int A, int B, int C)\u003E();\r\n\r\n\t\tforeach ( var face in mesh.Faces )\r\n\t\t\tfor ( var c = 1; c \u002B 1 \u003C face.Count; c\u002B\u002B )\r\n\t\t\t\ttris.Add( (face.Indices[0], face.Indices[c], face.Indices[c \u002B 1]) );\r\n\r\n\t\tvar limit = MathF.Max( edge, 0.05f );\r\n\r\n\t\tfloat Longest() =\u003E tris.Count == 0 ? 0f : tris.Max( t =\u003E MathF.Max( (positions[t.A] - positions[t.B]).Length,\r\n\t\t\tMathF.Max( (positions[t.B] - positions[t.C]).Length, (positions[t.C] - positions[t.A]).Length ) ) );\r\n\r\n\t\twhile ( Longest() \u003E limit \u0026\u0026 tris.Count * 4 \u003C= maxTriangles )\r\n\t\t{\r\n\t\t\tvar mids = new Dictionary\u003CEdgeKey, int\u003E();\r\n\t\t\tvar next = new List\u003C(int, int, int)\u003E( tris.Count * 4 );\r\n\r\n\t\t\tint Mid( int a, int b )\r\n\t\t\t{\r\n\t\t\t\tvar key = new EdgeKey( a, b );\r\n\r\n\t\t\t\tif ( !mids.TryGetValue( key, out var m ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tm = positions.Count;\r\n\t\t\t\t\tpositions.Add( (positions[a] \u002B positions[b]) * 0.5f );\r\n\t\t\t\t\tmids[key] = m;\r\n\t\t\t\t}\r\n\r\n\t\t\t\treturn m;\r\n\t\t\t}\r\n\r\n\t\t\tforeach ( var (a, b, c) in tris )\r\n\t\t\t{\r\n\t\t\t\tvar ab = Mid( a, b );\r\n\t\t\t\tvar bc = Mid( b, c );\r\n\t\t\t\tvar ca = Mid( c, a );\r\n\r\n\t\t\t\tnext.Add( (a, ab, ca) );\r\n\t\t\t\tnext.Add( (ab, b, bc) );\r\n\t\t\t\tnext.Add( (ca, bc, c) );\r\n\t\t\t\tnext.Add( (ab, bc, ca) );\r\n\t\t\t}\r\n\r\n\t\t\ttris = next;\r\n\t\t}\r\n\r\n\t\tvar result = new PolyMesh();\r\n\r\n\t\tforeach ( var p in positions )\r\n\t\t\tresult.AddVertex( p );\r\n\r\n\t\tforeach ( var (a, b, c) in tris )\r\n\t\t\tresult.AddFace( new[] { a, b, c }, null, 0 );\r\n\r\n\t\treturn result;\r\n\t}\r\n}\r\n"},{"Ident":"pooh.geppetto","Path":"Editor/Effigy/MeshClip.cs","FileName":"MeshClip.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":411088,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\n\nnamespace Effigy;\n\n/// \u003Csummary\u003E\n/// Keep the half of a mesh on one side of a plane, clipping faces that straddle it.\n///\n/// THIS IS A VIEW, NOT A FEATURE. Shell builds interiors nobody can look at; a cutaway hides the\n/// near half while looking, without writing anything into the history. Faces that cross the plane\n/// are clipped in place so the cut reads as a hard line rather than as missing polygons. There is\n/// no cap \u2014 an open cut is the point of a section view, and capping would pretend the kernel had\n/// booleaned a solid it has not.\n/// \u003C/summary\u003E\npublic static class MeshClip\n{\n\t/// \u003Csummary\u003E\n\t/// Everything on the positive side of the plane through \u003Cparamref name=\u0022origin\u0022/\u003E with\n\t/// \u003Cparamref name=\u0022normal\u0022/\u003E, including the plane itself. Vertices on the negative side are\n\t/// dropped; edges that cross emit a new vertex at the intersection.\n\t/// \u003C/summary\u003E\n\tpublic static PolyMesh Keep( PolyMesh mesh, Vec3 origin, Vec3 normal, float tolerance = 1e-5f )\n\t{\n\t\tif ( mesh is null )\n\t\t\tthrow new ArgumentNullException( nameof( mesh ) );\n\n\t\tvar n = normal.Normal;\n\n\t\tif ( n.LengthSquared \u003C 0.5f )\n\t\t\treturn mesh.Clone();\n\n\t\tvar result = new PolyMesh();\n\n\t\tforeach ( var face in mesh.Faces )\n\t\t{\n\t\t\tif ( face.Count \u003C 3 )\n\t\t\t\tcontinue;\n\n\t\t\tvar clipped = ClipFace( mesh, face, origin, n, tolerance );\n\n\t\t\tif ( clipped.Count \u003C 3 )\n\t\t\t\tcontinue;\n\n\t\t\tvar indices = new int[clipped.Count];\n\t\t\tvar uvs = new Vec2[clipped.Count];\n\n\t\t\tfor ( var i = 0; i \u003C clipped.Count; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tindices[i] = result.AddVertex( clipped[i].Position );\n\t\t\t\tuvs[i] = clipped[i].Uv;\n\t\t\t}\n\n\t\t\tresult.AddFace( indices, uvs, face.Material );\n\t\t}\n\n\t\treturn result;\n\t}\n\n\treadonly struct ClipVert\n\t{\n\t\tpublic readonly Vec3 Position;\n\t\tpublic readonly Vec2 Uv;\n\n\t\tpublic ClipVert( Vec3 position, Vec2 uv )\n\t\t{\n\t\t\tPosition = position;\n\t\t\tUv = uv;\n\t\t}\n\t}\n\n\tstatic List\u003CClipVert\u003E ClipFace( PolyMesh mesh, Face face, Vec3 origin, Vec3 normal, float tolerance )\n\t{\n\t\tvar polygon = new List\u003CClipVert\u003E( face.Count );\n\n\t\tfor ( var i = 0; i \u003C face.Count; i\u002B\u002B )\n\t\t{\n\t\t\tvar uv = face.UVs is not null \u0026\u0026 i \u003C face.UVs.Length ? face.UVs[i] : default;\n\t\t\tpolygon.Add( new ClipVert( mesh.Positions[face.Indices[i]], uv ) );\n\t\t}\n\n\t\t// Sutherland\u2013Hodgman against one plane: walk each edge, emit the intersection when the\n\t\t// edge crosses, and keep the vertex when it sits on the kept side.\n\t\tvar output = new List\u003CClipVert\u003E( polygon.Count \u002B 2 );\n\n\t\tfor ( var i = 0; i \u003C polygon.Count; i\u002B\u002B )\n\t\t{\n\t\t\tvar current = polygon[i];\n\t\t\tvar previous = polygon[( i \u002B polygon.Count - 1 ) % polygon.Count];\n\t\t\tvar currentDist = Vec3.Dot( current.Position - origin, normal );\n\t\t\tvar previousDist = Vec3.Dot( previous.Position - origin, normal );\n\t\t\tvar currentIn = currentDist \u003E= -tolerance;\n\t\t\tvar previousIn = previousDist \u003E= -tolerance;\n\n\t\t\tif ( currentIn )\n\t\t\t{\n\t\t\t\tif ( !previousIn )\n\t\t\t\t\toutput.Add( Intersect( previous, current, previousDist, currentDist ) );\n\n\t\t\t\toutput.Add( current );\n\t\t\t}\n\t\t\telse if ( previousIn )\n\t\t\t{\n\t\t\t\toutput.Add( Intersect( previous, current, previousDist, currentDist ) );\n\t\t\t}\n\t\t}\n\n\t\treturn output;\n\t}\n\n\tstatic ClipVert Intersect( ClipVert a, ClipVert b, float da, float db )\n\t{\n\t\tvar denom = da - db;\n\n\t\t// Both on the plane, or an edge so close the numbers vanish: pick B rather than divide.\n\t\tvar t = MathF.Abs( denom ) \u003C 1e-12f ? 1f : da / denom;\n\t\tt = Math.Clamp( t, 0f, 1f );\n\n\t\treturn new ClipVert( a.Position \u002B ( b.Position - a.Position ) * t, a.Uv \u002B ( b.Uv - a.Uv ) * t );\n\t}\n}\n"},{"Ident":"pooh.geppetto","Path":"Editor/Effigy/MeshSection.cs","FileName":"MeshSection.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":411088,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\n\nnamespace Effigy;\n\n/// \u003Csummary\u003E\n/// Where a mesh meets a plane, as line segments.\n///\n/// This exists for one job: showing the FOOTPRINT another body leaves on a face. Effigy does not\n/// union bodies, so an extrude standing on a slab is two separate solids that happen to overlap,\n/// and the slab\u0027s top face is one uninterrupted rectangle with no idea anything is sitting on it.\n/// Outlining only that rectangle makes a face with a block on it look completely clear.\n///\n/// Two cases, and both turn up constantly because they are the two ways bodies get stacked:\n/// a solid that PASSES THROUGH the plane, whose faces cross it and produce a chord each; and one\n/// that SITS ON it, whose bottom face lies in the plane and produces its own outline. Handling\n/// only the first would miss every part built by sketching on a face, which is most of them.\n/// \u003C/summary\u003E\npublic static class MeshSection\n{\n\t/// \u003Csummary\u003E\n\t/// Every segment where \u003Cparamref name=\u0022mesh\u0022/\u003E meets the plane through\n\t/// \u003Cparamref name=\u0022planeOrigin\u0022/\u003E with \u003Cparamref name=\u0022planeNormal\u0022/\u003E.\n\t/// \u003C/summary\u003E\n\tpublic static List\u003C(Vec3 A, Vec3 B)\u003E CrossSection( PolyMesh mesh, Vec3 planeOrigin, Vec3 planeNormal,\n\t\tfloat tolerance = 1e-4f )\n\t{\n\t\tvar segments = new List\u003C(Vec3, Vec3)\u003E();\n\n\t\tif ( mesh is null || mesh.Faces.Count == 0 )\n\t\t\treturn segments;\n\n\t\tvar normal = planeNormal.Normal;\n\n\t\tforeach ( var face in mesh.Faces )\n\t\t{\n\t\t\tif ( face.Count \u003C 3 )\n\t\t\t\tcontinue;\n\n\t\t\tvar distances = new float[face.Count];\n\t\t\tvar above = false;\n\t\t\tvar below = false;\n\n\t\t\tfor ( var i = 0; i \u003C face.Count; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tdistances[i] = Vec3.Dot( mesh.Positions[face.Indices[i]] - planeOrigin, normal );\n\n\t\t\t\tif ( distances[i] \u003E tolerance )\n\t\t\t\t\tabove = true;\n\t\t\t\telse if ( distances[i] \u003C -tolerance )\n\t\t\t\t\tbelow = true;\n\t\t\t}\n\n\t\t\tvar coplanar = !above \u0026\u0026 !below;\n\n\t\t\t// Sitting flat ON the plane: the face\u0027s own edges ARE the footprint.\n\t\t\tif ( coplanar )\n\t\t\t{\n\t\t\t\tfor ( var i = 0; i \u003C face.Count; i\u002B\u002B )\n\t\t\t\t{\n\t\t\t\t\tsegments.Add( (mesh.Positions[face.Indices[i]],\n\t\t\t\t\t\tmesh.Positions[face.Indices[(i \u002B 1) % face.Count]]) );\n\t\t\t\t}\n\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// STRICTLY BOTH SIDES, or this face does not cross the plane - it only touches it.\n\t\t\t// A block standing on a slab touches with all four of its side walls, each along its\n\t\t\t// bottom edge, and every one of those edges is already an edge of the coplanar bottom\n\t\t\t// face. Without this the whole footprint gets drawn twice.\n\t\t\tif ( !above || !below )\n\t\t\t\tcontinue;\n\n\t\t\t// Passing through it: collect where the face\u0027s edges cross, which for a convex face is\n\t\t\t// exactly two points and therefore one chord.\n\t\t\tvar crossings = new List\u003CVec3\u003E( 2 );\n\n\t\t\tfor ( var i = 0; i \u003C face.Count; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar a = mesh.Positions[face.Indices[i]];\n\t\t\t\tvar b = mesh.Positions[face.Indices[(i \u002B 1) % face.Count]];\n\t\t\t\tvar da = distances[i];\n\t\t\t\tvar db = distances[(i \u002B 1) % face.Count];\n\n\t\t\t\t// A corner sitting on the plane is reported once, by the edge that arrives at it -\n\t\t\t\t// counting it twice would leave a zero-length segment behind.\n\t\t\t\tif ( MathF.Abs( da ) \u003C= tolerance )\n\t\t\t\t{\n\t\t\t\t\tcrossings.Add( a );\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif ( MathF.Abs( db ) \u003C= tolerance || da * db \u003E 0f )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tcrossings.Add( a \u002B (b - a) * (da / (da - db)) );\n\t\t\t}\n\n\t\t\tfor ( var i = 0; i \u002B 1 \u003C crossings.Count; i \u002B= 2 )\n\t\t\t{\n\t\t\t\tif ( (crossings[i \u002B 1] - crossings[i]).LengthSquared \u003E tolerance * tolerance )\n\t\t\t\t\tsegments.Add( (crossings[i], crossings[i \u002B 1]) );\n\t\t\t}\n\t\t}\n\n\t\treturn segments;\n\t}\n}\n"},{"Ident":"pooh.geppetto","Path":"Editor/Effigy/ObjWriter.cs","FileName":"ObjWriter.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":411088,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.IO;\nusing System.Text;\n\nnamespace Effigy;\n\n/// \u003Csummary\u003E\n/// Wavefront OBJ export \u2014 the DEBUG and interchange format, not the export path.\n///\n/// THIS IS NO LONGER HOW MODELS REACH s\u0026amp;box. OBJ cannot carry bones or vertex weights, so it stops\n/// being viable the moment a model is rigged, and rigging is in scope. SmdWriter is the export\n/// path for both static and skinned models; see the note at the top of it for why one format\n/// covers both.\n///\n/// What OBJ is still the best tool for, and why it stays:\n///\n///   It is the only format here that PRESERVES QUADS. SMD triangulates on the way out, so an OBJ is\n///   the only way to look at the cage as the kernel actually holds it \u2014 which is exactly what you\n///   need when checking whether a subdivision result is right. Open it in Blender and the topology\n///   is there to read.\n///\n///   The test suite writes one per primitive for that reason, and round-trips them to prove the\n///   writer emits something parseable with its topology intact.\n///\n/// Godot needs neither path \u2014 there the kernel hands vertices to ArrayMesh directly.\n/// \u003C/summary\u003E\npublic static class ObjWriter\n{\n\t/// \u003Csummary\u003EKept as an alias so existing callers do not have to know where this moved to.\u003C/summary\u003E\n\tpublic const float DefaultSmoothingAngleDegrees = MeshNormals.DefaultSmoothingAngleDegrees;\n\n\t/// \u003Csummary\u003EWhat an unnamed slot is called. Shared with SmdWriter and DmxWriter so a model\n\t/// exported three ways names its materials the same three times.\u003C/summary\u003E\n\tpublic static string DefaultMaterialName( int slot ) =\u003E $\u0022material_{slot}\u0022;\n\n\tpublic static void WriteFile( PolyMesh mesh, string path, string objectName = \u0022model\u0022,\n\t\tfloat smoothingAngleDegrees = DefaultSmoothingAngleDegrees, Func\u003Cint, string\u003E materialName = null )\n\t{\n\t\tFile.WriteAllText( path, Write( mesh, objectName, smoothingAngleDegrees, materialName ) );\n\t}\n\n\t/// \u003Cparam name=\u0022materialName\u0022\u003EWhat to call each material slot. Defaults to material_0, material_1\n\t/// and so on \u2014 a name a person chose is the difference between binding by meaning and binding by\n\t/// number in whatever the model lands in.\u003C/param\u003E\n\tpublic static string Write( PolyMesh mesh, string objectName = \u0022model\u0022,\n\t\tfloat smoothingAngleDegrees = DefaultSmoothingAngleDegrees, Func\u003Cint, string\u003E materialName = null )\n\t{\n\t\tvar sb = new StringBuilder();\n\n\t\tsb.Append( \u0022# generated by Effigy\\n\u0022 );\n\t\tsb.Append( $\u0022# {mesh.VertexCount} vertices, {mesh.FaceCount} faces\\n\u0022 );\n\n\t\tvar offsets = (Vertices: 0, UVs: 0, Normals: 0);\n\t\tAppendPiece( sb, mesh, objectName, smoothingAngleDegrees, materialName, ref offsets );\n\n\t\treturn sb.ToString();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Several named pieces in one file, each under its own \u003Cc\u003Eo\u003C/c\u003E marker \u2014 which is what makes\n\t/// it come back as several parts rather than one welded lump.\n\t///\n\t/// THE MERGE IS THE LOSSY STEP, not the format. A part-segmentation export exists precisely so\n\t/// that a hand is a hand and a torso is a torso, and flattening fourteen of those into one mesh\n\t/// on the way out throws away the only thing that made the file worth segmenting: everything\n\t/// downstream that works per part \u2014 rigging a bone to a body, hiding one while you sculpt\n\t/// another, a Remesh with a budget of its own \u2014 has nothing left to work per part ON.\n\t///\n\t/// OBJ\u0027s indices are file-global rather than per-object, which is the entire reason this cannot\n\t/// be done by concatenating what \u003Csee cref=\u0022Write(PolyMesh, string, float, Func{int, string})\u0022/\u003E\n\t/// returns: every piece after the first would reference the first one\u0027s vertices.\n\t/// \u003C/summary\u003E\n\tpublic static void WriteFile( IReadOnlyList\u003CObjReader.ObjPiece\u003E pieces, string path,\n\t\tfloat smoothingAngleDegrees = DefaultSmoothingAngleDegrees, Func\u003Cint, string\u003E materialName = null )\n\t{\n\t\tFile.WriteAllText( path, Write( pieces, smoothingAngleDegrees, materialName ) );\n\t}\n\n\t/// \u003Cinheritdoc cref=\u0022WriteFile(IReadOnlyList{ObjReader.ObjPiece}, string, float, Func{int, string})\u0022/\u003E\n\tpublic static string Write( IReadOnlyList\u003CObjReader.ObjPiece\u003E pieces,\n\t\tfloat smoothingAngleDegrees = DefaultSmoothingAngleDegrees, Func\u003Cint, string\u003E materialName = null )\n\t{\n\t\tvar sb = new StringBuilder();\n\t\tvar offsets = (Vertices: 0, UVs: 0, Normals: 0);\n\n\t\tvar vertices = 0;\n\t\tvar faces = 0;\n\n\t\tforeach ( var piece in pieces )\n\t\t{\n\t\t\tvertices \u002B= piece.Mesh?.VertexCount ?? 0;\n\t\t\tfaces \u002B= piece.Mesh?.FaceCount ?? 0;\n\t\t}\n\n\t\tsb.Append( \u0022# generated by Effigy\\n\u0022 );\n\t\tsb.Append( $\u0022# {vertices} vertices, {faces} faces, {pieces.Count} objects\\n\u0022 );\n\n\t\tfor ( var i = 0; i \u003C pieces.Count; i\u002B\u002B )\n\t\t{\n\t\t\tvar piece = pieces[i];\n\n\t\t\tif ( piece.Mesh is null || piece.Mesh.FaceCount == 0 )\n\t\t\t\tcontinue;\n\n\t\t\t// An unnamed piece still needs a marker, or it merges into the one before it on the way\n\t\t\t// back in. ReadPieces hands back \u0022\u0022 for faces that appeared before any o/g line.\n\t\t\tvar name = string.IsNullOrWhiteSpace( piece.Name ) ? $\u0022part_{i}\u0022 : piece.Name;\n\n\t\t\tAppendPiece( sb, piece.Mesh, name, smoothingAngleDegrees, materialName, ref offsets );\n\t\t}\n\n\t\treturn sb.ToString();\n\t}\n\n\t/// \u003Csummary\u003EOne object\u0027s worth of the file. The offsets go in as the counts written so far and\n\t/// come back raised by this piece\u0027s own, because OBJ numbers v/vt/vn across the whole file.\n\t/// \u003C/summary\u003E\n\tprivate static void AppendPiece( StringBuilder sb, PolyMesh mesh, string objectName,\n\t\tfloat smoothingAngleDegrees, Func\u003Cint, string\u003E materialName,\n\t\tref (int Vertices, int UVs, int Normals) offsets )\n\t{\n\t\tvar c = CultureInfo.InvariantCulture;\n\n\t\tsb.Append( $\u0022o {objectName}\\n\u0022 );\n\n\t\tfor ( var i = 0; i \u003C mesh.Positions.Count; i\u002B\u002B )\n\t\t{\n\t\t\tvar p = mesh.Positions[i];\n\n\t\t\tsb.Append( string.Format( c, \u0022v {0:0.######} {1:0.######} {2:0.######}\u0022, p.x, p.y, p.z ) );\n\n\t\t\t// Vertex colour, when there is one. OBJ vertex colour is RGB, and \u0022no paint\u0022 is WHITE \u2014\n\t\t\t// the consumer multiplies the material by the colour, so white means \u0022unchanged\u0022 and\n\t\t\t// coverage fades the vertex from white toward the paint colour.\n\t\t\tif ( mesh.VertexColors is not null \u0026\u0026 i \u003C mesh.VertexColors.Length )\n\t\t\t{\n\t\t\t\tvar tint = mesh.VertexColors[i].Tint();\n\n\t\t\t\tsb.Append( string.Format( c, \u0022 {0:0.######} {1:0.######} {2:0.######}\u0022,\n\t\t\t\t\ttint.x, tint.y, tint.z ) );\n\t\t\t}\n\n\t\t\tsb.Append( \u0027\\n\u0027 );\n\t\t}\n\n\t\t// UVs are per corner, so the same value recurs constantly. Deduping keeps the file to a\n\t\t// sane size without changing what it means.\n\t\tvar uvIndex = new Dictionary\u003C(long, long), int\u003E();\n\t\tvar faceUVRefs = new int[mesh.FaceCount][];\n\n\t\tfor ( var fi = 0; fi \u003C mesh.FaceCount; fi\u002B\u002B )\n\t\t{\n\t\t\tvar f = mesh.Faces[fi];\n\t\t\tfaceUVRefs[fi] = new int[f.Count];\n\n\t\t\tfor ( var i = 0; i \u003C f.Count; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar uv = f.UVs[i];\n\n\t\t\t\t// OBJ puts the UV origin at the bottom-left where Effigy\u0027s is top-left, so V is\n\t\t\t\t// flipped on the way out \u2014 the same flip FbxWriter makes for the same reason, and the\n\t\t\t\t// one DmxWriter avoids by leaving flipVCoordinates off and writing V as-is. Without it\n\t\t\t\t// a compiled static model samples its texture upside down relative to the viewport.\n\t\t\t\tvar flippedV = 1f - uv.y;\n\t\t\t\tvar key = ((long)MathF.Round( uv.x * 1e5f ), (long)MathF.Round( flippedV * 1e5f ));\n\n\t\t\t\tif ( !uvIndex.TryGetValue( key, out var idx ) )\n\t\t\t\t{\n\t\t\t\t\tidx = uvIndex.Count;\n\t\t\t\t\tuvIndex[key] = idx;\n\t\t\t\t\tsb.Append( string.Format( c, \u0022vt {0:0.######} {1:0.######}\\n\u0022, uv.x, flippedV ) );\n\t\t\t\t}\n\n\t\t\t\tfaceUVRefs[fi][i] = idx;\n\t\t\t}\n\t\t}\n\n\t\tvar (cornerNormals, normals) = MeshNormals.ComputeCornerNormals( mesh, smoothingAngleDegrees );\n\n\t\tforeach ( var n in normals )\n\t\t\tsb.Append( string.Format( c, \u0022vn {0:0.######} {1:0.######} {2:0.######}\\n\u0022, n.x, n.y, n.z ) );\n\n\t\tvar currentMaterial = int.MinValue;\n\n\t\tfor ( var fi = 0; fi \u003C mesh.FaceCount; fi\u002B\u002B )\n\t\t{\n\t\t\tvar f = mesh.Faces[fi];\n\n\t\t\tif ( f.Material != currentMaterial )\n\t\t\t{\n\t\t\t\tcurrentMaterial = f.Material;\n\t\t\t\tsb.Append( $\u0022usemtl {(materialName ?? DefaultMaterialName)( currentMaterial )}\\n\u0022 );\n\t\t\t}\n\n\t\t\tsb.Append( \u0027f\u0027 );\n\n\t\t\tfor ( var i = 0; i \u003C f.Count; i\u002B\u002B )\n\t\t\t{\n\t\t\t\t// OBJ indices are 1-based, and counted from the top of the FILE rather than from\n\t\t\t\t// the top of this object \u2014 hence the offsets, which are zero for a one-piece write.\n\t\t\t\tsb.Append( $\u0022 {f.Indices[i] \u002B 1 \u002B offsets.Vertices}\u0022\n\t\t\t\t\t\u002B $\u0022/{faceUVRefs[fi][i] \u002B 1 \u002B offsets.UVs}\u0022\n\t\t\t\t\t\u002B $\u0022/{cornerNormals[fi][i] \u002B 1 \u002B offsets.Normals}\u0022 );\n\t\t\t}\n\n\t\t\tsb.Append( \u0027\\n\u0027 );\n\t\t}\n\n\t\toffsets = (offsets.Vertices \u002B mesh.VertexCount, offsets.UVs \u002B uvIndex.Count,\n\t\t\toffsets.Normals \u002B normals.Count);\n\t}\n\n}\n\n/// \u003Csummary\u003E\n/// Wavefront OBJ reader \u2014 the import path, and the round-trip for tests.\n///\n/// Understands what \u003Csee cref=\u0022ObjWriter\u0022/\u003E emits, plus the subset a Meshy or Blender export\n/// actually uses: positions, optional vertex colour, UVs, faces (with or without vt/vn), and\n/// \u003Cc\u003Eusemtl material_N\u003C/c\u003E. Everything else on a line is ignored rather than refused, because\n/// a real OBJ is full of \u003Cc\u003Eo\u003C/c\u003E / \u003Cc\u003Eg\u003C/c\u003E / \u003Cc\u003Es\u003C/c\u003E / \u003Cc\u003Evn\u003C/c\u003E / \u003Cc\u003Emtllib\u003C/c\u003E that do not\n/// change the solid.\n///\n/// V IS FLIPPED, matching the writer. OBJ\u0027s UV origin is bottom-left; Effigy\u0027s is top-left.\n/// Reading without the flip would import every textured mesh upside down, and a writer\u2192reader\n/// round trip would invert V twice and look correct by accident.\n/// \u003C/summary\u003E\npublic static class ObjReader\n{\n\tpublic static PolyMesh ReadFile( string path ) =\u003E Read( File.ReadAllText( path ) );\n\n\t/// \u003Csummary\u003EOne named lump of an OBJ \u2014 an \u003Cc\u003Eo\u003C/c\u003E object or a \u003Cc\u003Eg\u003C/c\u003E group, with the faces\n\t/// that followed it. See \u003Csee cref=\u0022ReadPieces\u0022/\u003E.\u003C/summary\u003E\n\tpublic readonly struct ObjPiece\n\t{\n\t\tpublic readonly string Name;\n\t\tpublic readonly PolyMesh Mesh;\n\n\t\tpublic ObjPiece( string name, PolyMesh mesh )\n\t\t{\n\t\t\tName = name;\n\t\t\tMesh = mesh;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The whole file as one mesh, objects and groups fused. What every caller wanted before\n\t/// imports could be split, and what a writer\u0027s round-trip still compares against.\n\t/// \u003C/summary\u003E\n\tpublic static PolyMesh Read( string text )\n\t{\n\t\tvar parsed = Parse( text );\n\t\tvar mesh = new PolyMesh { Positions = parsed.Positions };\n\n\t\tforeach ( var f in parsed.Faces )\n\t\t\tmesh.AddFace( f.Indices, f.UVs, f.Material );\n\n\t\tif ( parsed.SawColor )\n\t\t\tmesh.VertexColors = parsed.Colors.ToArray();\n\n\t\treturn mesh;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The file split at its \u003Cc\u003Eo\u003C/c\u003E and \u003Cc\u003Eg\u003C/c\u003E lines, one mesh per lump.\n\t///\n\t/// WHY THIS EXISTS: an OBJ\u0027s vertex indices are global to the FILE, not to the object, so an\n\t/// exporter that carefully kept a character\u0027s eyelids, brows and hair as separate objects hands\n\t/// us one flat pile of triangles with the boundaries recorded only in these marker lines.\n\t/// Reading it with \u003Csee cref=\u0022Read\u0022/\u003E throws that structure away and produces one welded body \u2014\n\t/// which is exactly the body you cannot build a tidy feature tree out of, and cannot hide,\n\t/// re-material or weight a piece of on its own.\n\t///\n\t/// Each piece gets its own vertex list, renumbered, holding only the vertices its faces\n\t/// actually use. Faces before the first marker land in a piece with an empty name; the caller\n\t/// decides what to call that, since only it knows what the file was called.\n\t///\n\t/// A file with no markers at all comes back as one unnamed piece, so a caller can always take\n\t/// this path rather than branching on whether the exporter bothered.\n\t/// \u003C/summary\u003E\n\tpublic static List\u003CObjPiece\u003E ReadPieces( string text )\n\t{\n\t\tvar parsed = Parse( text );\n\t\tvar pieces = new List\u003CObjPiece\u003E();\n\n\t\t// Grouped by index rather than by name so two objects sharing a name stay two pieces \u2014\n\t\t// a duplicated name is an exporter\u0027s business, not a reason to weld geometry together.\n\t\tvar byGroup = new Dictionary\u003Cint, List\u003CObjFace\u003E\u003E();\n\t\tvar order = new List\u003Cint\u003E();\n\n\t\tforeach ( var f in parsed.Faces )\n\t\t{\n\t\t\tif ( !byGroup.TryGetValue( f.Group, out var list ) )\n\t\t\t{\n\t\t\t\tbyGroup[f.Group] = list = new List\u003CObjFace\u003E();\n\t\t\t\torder.Add( f.Group );\n\t\t\t}\n\n\t\t\tlist.Add( f );\n\t\t}\n\n\t\tforeach ( var group in order )\n\t\t{\n\t\t\tvar faces = byGroup[group];\n\t\t\tvar mesh = new PolyMesh();\n\t\t\tvar remap = new Dictionary\u003Cint, int\u003E();\n\t\t\tvar colors = parsed.SawColor ? new List\u003CVec4\u003E() : null;\n\n\t\t\tforeach ( var f in faces )\n\t\t\t{\n\t\t\t\tvar indices = new int[f.Indices.Length];\n\n\t\t\t\tfor ( var i = 0; i \u003C f.Indices.Length; i\u002B\u002B )\n\t\t\t\t{\n\t\t\t\t\tvar source = f.Indices[i];\n\n\t\t\t\t\tif ( !remap.TryGetValue( source, out var local ) )\n\t\t\t\t\t{\n\t\t\t\t\t\tlocal = mesh.AddVertex( parsed.Positions[source] );\n\t\t\t\t\t\tremap[source] = local;\n\t\t\t\t\t\tcolors?.Add( parsed.Colors[source] );\n\t\t\t\t\t}\n\n\t\t\t\t\tindices[i] = local;\n\t\t\t\t}\n\n\t\t\t\tmesh.AddFace( indices, f.UVs, f.Material );\n\t\t\t}\n\n\t\t\tif ( colors is not null )\n\t\t\t\tmesh.VertexColors = colors.ToArray();\n\n\t\t\tpieces.Add( new ObjPiece( group \u003E= 0 ? parsed.GroupNames[group] : \u0022\u0022, mesh ) );\n\t\t}\n\n\t\treturn pieces;\n\t}\n\n\treadonly struct ObjFace\n\t{\n\t\tpublic readonly int[] Indices;\n\t\tpublic readonly Vec2[] UVs;\n\t\tpublic readonly int Material;\n\t\tpublic readonly int Group;\n\n\t\tpublic ObjFace( int[] indices, Vec2[] uvs, int material, int group )\n\t\t{\n\t\t\tIndices = indices;\n\t\t\tUVs = uvs;\n\t\t\tMaterial = material;\n\t\t\tGroup = group;\n\t\t}\n\t}\n\n\tsealed class ObjData\n\t{\n\t\tpublic List\u003CVec3\u003E Positions = new();\n\t\tpublic List\u003CVec4\u003E Colors = new();\n\t\tpublic List\u003CVec2\u003E UVs = new();\n\t\tpublic List\u003CObjFace\u003E Faces = new();\n\t\tpublic List\u003Cstring\u003E GroupNames = new();\n\t\tpublic bool SawColor;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// One pass over the text. Both readers above run on this, so a fused read and a split read\n\t/// cannot disagree about what the file says \u2014 which is the whole reason it is factored out\n\t/// rather than written twice.\n\t/// \u003C/summary\u003E\n\tstatic ObjData Parse( string text )\n\t{\n\t\tvar data = new ObjData();\n\t\tvar material = 0;\n\t\tvar group = -1;\n\n\t\t// One pass over the string as spans. The string.Split / Trim version this replaced did\n\t\t// three allocations a line on a dense file \u2014 the line, the token array, and every token\n\t\t// itself \u2014 which on a million-face OBJ is tens of millions of objects for the GC to chew.\n\t\t// Spans keep the whole parse allocation-free except for the data it is actually building.\n\t\tvar remaining = text.AsSpan();\n\n\t\twhile ( remaining.Length \u003E 0 )\n\t\t{\n\t\t\tvar newline = remaining.IndexOf( \u0027\\n\u0027 );\n\t\t\tvar line = newline \u003C 0 ? remaining : remaining.Slice( 0, newline );\n\t\t\tremaining = newline \u003C 0 ? default : remaining.Slice( newline \u002B 1 );\n\n\t\t\tline = line.Trim();\n\n\t\t\tif ( line.Length == 0 || line[0] == \u0027#\u0027 )\n\t\t\t\tcontinue;\n\n\t\t\tvar space = line.IndexOf( \u0027 \u0027 );\n\t\t\tvar head = space \u003C 0 ? line : line.Slice( 0, space );\n\t\t\tvar rest = space \u003C 0 ? default : line.Slice( space \u002B 1 );\n\n\t\t\tswitch ( head[0] )\n\t\t\t{\n\t\t\t\tcase \u0027v\u0027:\n\t\t\t\t{\n\t\t\t\t\tif ( head.Length == 2 )\n\t\t\t\t\t{\n\t\t\t\t\t\t// vt: writer flips V on the way out; this un-flips it. External OBJs\n\t\t\t\t\t\t// (bottom-left origin) land in Effigy space the same way. vn (normals) is\n\t\t\t\t\t\t// skipped, as it always was.\n\t\t\t\t\t\tif ( head[1] == \u0027t\u0027 )\n\t\t\t\t\t\t\tdata.UVs.Add( new Vec2( NextFloat( ref rest ), 1f - NextFloat( ref rest ) ) );\n\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tdata.Positions.Add( new Vec3(\n\t\t\t\t\t\tNextFloat( ref rest ),\n\t\t\t\t\t\tNextFloat( ref rest ),\n\t\t\t\t\t\tNextFloat( ref rest ) ) );\n\n\t\t\t\t\tif ( CountTokens( rest ) \u003E= 3 )\n\t\t\t\t\t{\n\t\t\t\t\t\tdata.SawColor = true;\n\t\t\t\t\t\tdata.Colors.Add( new Vec4(\n\t\t\t\t\t\t\tNextFloat( ref rest ),\n\t\t\t\t\t\t\tNextFloat( ref rest ),\n\t\t\t\t\t\t\tNextFloat( ref rest ),\n\t\t\t\t\t\t\t1f ) );\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\tdata.Colors.Add( new Vec4( 1f, 1f, 1f, 1f ) );\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcase \u0027f\u0027:\n\t\t\t\t{\n\t\t\t\t\tvar n = CountTokens( rest );\n\n\t\t\t\t\tif ( n \u003C 3 )\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tvar indices = new int[n];\n\t\t\t\t\tvar faceUVs = new Vec2[n];\n\n\t\t\t\t\tfor ( var i = 0; i \u003C n; i\u002B\u002B )\n\t\t\t\t\t{\n\t\t\t\t\t\tNextToken( ref rest, out var corner );\n\n\t\t\t\t\t\tvar slash = corner.IndexOf( \u0027/\u0027 );\n\t\t\t\t\t\tindices[i] = ObjIndex( slash \u003C 0 ? corner : corner.Slice( 0, slash ), data.Positions.Count );\n\n\t\t\t\t\t\tif ( slash \u003E= 0 )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvar after = corner.Slice( slash \u002B 1 );\n\t\t\t\t\t\t\tvar second = after.IndexOf( \u0027/\u0027 );\n\t\t\t\t\t\t\tvar vt = second \u003C 0 ? after : after.Slice( 0, second );\n\n\t\t\t\t\t\t\tif ( vt.Length \u003E 0 \u0026\u0026 data.UVs.Count \u003E 0 )\n\t\t\t\t\t\t\t\tfaceUVs[i] = data.UVs[ObjIndex( vt, data.UVs.Count )];\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tdata.Faces.Add( new ObjFace( indices, faceUVs, material, group ) );\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\t// BOTH MARKERS START A PIECE. Exporters disagree about which one means \u0022object\u0022:\n\t\t\t\t// Blender writes \u0060o\u0060 per object and \u0060g\u0060 for its vertex groups, other tools only\n\t\t\t\t// ever write \u0060g\u0060. Treating either as a boundary is what makes the split work on\n\t\t\t\t// files this tool did not write, which is the only kind it gets.\n\t\t\t\tcase \u0027o\u0027:\n\t\t\t\tcase \u0027g\u0027:\n\t\t\t\t\tdata.GroupNames.Add( rest.Length \u003E 0 ? JoinTokens( rest ) : \u0022\u0022 );\n\t\t\t\t\tgroup = data.GroupNames.Count - 1;\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \u0027u\u0027:\n\t\t\t\t\t// usemtl \u2014 the only u-keyword the reader acts on.\n\t\t\t\t\tif ( head.SequenceEqual( \u0022usemtl\u0022.AsSpan() ) )\n\t\t\t\t\t\tmaterial = SlotFromMaterialName( NextToken( ref rest, out var name ) ? name.ToString() : \u0022\u0022, material );\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\treturn data;\n\t}\n\n\t/// \u003Csummary\u003EThe next whitespace-delimited token, advancing \u003Cparamref name=\u0022s\u0022/\u003E past it.\u003C/summary\u003E\n\tstatic bool NextToken( ref ReadOnlySpan\u003Cchar\u003E s, out ReadOnlySpan\u003Cchar\u003E token )\n\t{\n\t\twhile ( s.Length \u003E 0 \u0026\u0026 (s[0] == \u0027 \u0027 || s[0] == \u0027\\t\u0027) )\n\t\t\ts = s.Slice( 1 );\n\n\t\tif ( s.Length == 0 )\n\t\t{\n\t\t\ttoken = default;\n\t\t\treturn false;\n\t\t}\n\n\t\tvar end = 0;\n\n\t\twhile ( end \u003C s.Length \u0026\u0026 s[end] != \u0027 \u0027 \u0026\u0026 s[end] != \u0027\\t\u0027 )\n\t\t\tend\u002B\u002B;\n\n\t\ttoken = s.Slice( 0, end );\n\t\ts = s.Slice( end );\n\t\treturn true;\n\t}\n\n\t/// \u003Csummary\u003EThe next token as a float.\u003C/summary\u003E\n\tstatic float NextFloat( ref ReadOnlySpan\u003Cchar\u003E s )\n\t{\n\t\tNextToken( ref s, out var token );\n\t\treturn ParseFloat( token );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A decimal float with no culture and no allocation.\n\t///\n\t/// The reader used \u003Cc\u003Efloat.Parse\u003C/c\u003E, which on a dense OBJ is the whole of the load time \u2014 a\n\t/// million vertices is a few million culture-aware, correctly-rounding parses into a general\n\t/// number machinery. OBJ numbers are plain decimals, so the digits are accumulated into an\n\t/// exact integer and scaled by a power of ten, which matches \u003Cc\u003Efloat.Parse\u003C/c\u003E on every value a\n\t/// Wavefront file actually contains and runs many times faster.\n\t/// \u003C/summary\u003E\n\tstatic float ParseFloat( ReadOnlySpan\u003Cchar\u003E s )\n\t{\n\t\tvar i = 0;\n\t\tvar len = s.Length;\n\n\t\tvar negative = false;\n\n\t\tif ( i \u003C len \u0026\u0026 (s[i] == \u0027-\u0027 || s[i] == \u0027\u002B\u0027) )\n\t\t{\n\t\t\tnegative = s[i] == \u0027-\u0027;\n\t\t\ti\u002B\u002B;\n\t\t}\n\n\t\t// Integer digits, then fraction digits, accumulated into one integer. Stopped at 15 digits:\n\t\t// below 2^53 the double conversion is exact, and 15 significant figures is already more than\n\t\t// a float can carry, so nothing is lost. Digits past that only move the decimal point.\n\t\tconst long Cap = 1_000_000_000_000_000L;\n\t\tlong mantissa = 0;\n\t\tvar digits = 0;\n\t\tvar intDigits = 0;\n\n\t\twhile ( i \u003C len \u0026\u0026 s[i] \u003E= \u00270\u0027 \u0026\u0026 s[i] \u003C= \u00279\u0027 )\n\t\t{\n\t\t\tif ( mantissa \u003C Cap )\n\t\t\t{\n\t\t\t\tmantissa = mantissa * 10 \u002B (s[i] - \u00270\u0027);\n\t\t\t\tdigits\u002B\u002B;\n\t\t\t}\n\n\t\t\tintDigits\u002B\u002B;\n\t\t\ti\u002B\u002B;\n\t\t}\n\n\t\tif ( i \u003C len \u0026\u0026 s[i] == \u0027.\u0027 )\n\t\t{\n\t\t\ti\u002B\u002B;\n\n\t\t\twhile ( i \u003C len \u0026\u0026 s[i] \u003E= \u00270\u0027 \u0026\u0026 s[i] \u003C= \u00279\u0027 )\n\t\t\t{\n\t\t\t\tif ( mantissa \u003C Cap )\n\t\t\t\t{\n\t\t\t\t\tmantissa = mantissa * 10 \u002B (s[i] - \u00270\u0027);\n\t\t\t\t\tdigits\u002B\u002B;\n\t\t\t\t}\n\n\t\t\t\ti\u002B\u002B;\n\t\t\t}\n\t\t}\n\n\t\t// Where the decimal point sits relative to the accumulated digits.\n\t\tvar exponent = intDigits - digits;\n\n\t\tif ( i \u003C len \u0026\u0026 (s[i] == \u0027e\u0027 || s[i] == \u0027E\u0027) )\n\t\t{\n\t\t\ti\u002B\u002B;\n\n\t\t\tvar expNegative = false;\n\n\t\t\tif ( i \u003C len \u0026\u0026 (s[i] == \u0027-\u0027 || s[i] == \u0027\u002B\u0027) )\n\t\t\t{\n\t\t\t\texpNegative = s[i] == \u0027-\u0027;\n\t\t\t\ti\u002B\u002B;\n\t\t\t}\n\n\t\t\tvar e = 0;\n\n\t\t\twhile ( i \u003C len \u0026\u0026 s[i] \u003E= \u00270\u0027 \u0026\u0026 s[i] \u003C= \u00279\u0027 )\n\t\t\t{\n\t\t\t\te = e * 10 \u002B (s[i] - \u00270\u0027);\n\t\t\t\ti\u002B\u002B;\n\t\t\t}\n\n\t\t\texponent \u002B= expNegative ? -e : e;\n\t\t}\n\n\t\tvar value = (double)mantissa * Pow10( exponent );\n\n\t\treturn (float)(negative ? -value : value);\n\t}\n\n\tstatic readonly double[] Pow10Positive = BuildPow10();\n\n\tstatic double[] BuildPow10()\n\t{\n\t\tvar table = new double[39];\n\t\ttable[0] = 1.0;\n\n\t\tfor ( var i = 1; i \u003C table.Length; i\u002B\u002B )\n\t\t\ttable[i] = table[i - 1] * 10.0;\n\n\t\treturn table;\n\t}\n\n\t/// \u003Csummary\u003E10 to the power of \u003Cparamref name=\u0022e\u0022/\u003E, from a table for the exponents a mesh\n\t/// coordinate uses and \u003Csee cref=\u0022Math.Pow\u0022/\u003E for the rest.\u003C/summary\u003E\n\tstatic double Pow10( int e )\n\t{\n\t\tif ( e \u003E= 0 \u0026\u0026 e \u003C Pow10Positive.Length )\n\t\t\treturn Pow10Positive[e];\n\n\t\tif ( e \u003C 0 \u0026\u0026 -e \u003C Pow10Positive.Length )\n\t\t\treturn 1.0 / Pow10Positive[-e];\n\n\t\treturn Math.Pow( 10.0, e );\n\t}\n\n\t/// \u003Csummary\u003EHow many whitespace-delimited tokens remain.\u003C/summary\u003E\n\tstatic int CountTokens( ReadOnlySpan\u003Cchar\u003E s )\n\t{\n\t\tvar count = 0;\n\n\t\twhile ( NextToken( ref s, out _ ) )\n\t\t\tcount\u002B\u002B;\n\n\t\treturn count;\n\t}\n\n\t/// \u003Csummary\u003EThe remaining tokens rejoined with single spaces \u2014 what string.Join over the split\n\t/// produced before.\u003C/summary\u003E\n\tstatic string JoinTokens( ReadOnlySpan\u003Cchar\u003E s )\n\t{\n\t\tvar builder = new StringBuilder();\n\n\t\twhile ( NextToken( ref s, out var token ) )\n\t\t{\n\t\t\tif ( builder.Length \u003E 0 )\n\t\t\t\tbuilder.Append( \u0027 \u0027 );\n\n\t\t\tbuilder.Append( token );\n\t\t}\n\n\t\treturn builder.ToString();\n\t}\n\n\t/// \u003Csummary\u003EWavefront indices are 1-based, and negative counts back from the last element.\u003C/summary\u003E\n\tstatic int ObjIndex( ReadOnlySpan\u003Cchar\u003E token, int count )\n\t{\n\t\tvar n = ParseInt( token );\n\t\treturn n \u003E 0 ? n - 1 : count \u002B n;\n\t}\n\n\t/// \u003Csummary\u003EA plain base-10 integer, no culture, no allocation. Face indices only, so small.\u003C/summary\u003E\n\tstatic int ParseInt( ReadOnlySpan\u003Cchar\u003E s )\n\t{\n\t\tvar i = 0;\n\t\tvar negative = false;\n\n\t\tif ( i \u003C s.Length \u0026\u0026 (s[i] == \u0027-\u0027 || s[i] == \u0027\u002B\u0027) )\n\t\t{\n\t\t\tnegative = s[i] == \u0027-\u0027;\n\t\t\ti\u002B\u002B;\n\t\t}\n\n\t\tvar value = 0;\n\n\t\twhile ( i \u003C s.Length )\n\t\t{\n\t\t\tvalue = value * 10 \u002B (s[i] - \u00270\u0027);\n\t\t\ti\u002B\u002B;\n\t\t}\n\n\t\treturn negative ? -value : value;\n\t}\n\n\t/// \u003Csummary\u003E\u003Csee cref=\u0022ObjWriter.DefaultMaterialName\u0022/\u003E is \u003Cc\u003Ematerial_N\u003C/c\u003E; anything else\n\t/// leaves the current slot alone rather than inventing one.\u003C/summary\u003E\n\tstatic int SlotFromMaterialName( string name, int current )\n\t{\n\t\tconst string prefix = \u0022material_\u0022;\n\n\t\tif ( name.StartsWith( prefix, StringComparison.OrdinalIgnoreCase )\n\t\t\t\u0026\u0026 int.TryParse( name.AsSpan( prefix.Length ), NumberStyles.Integer, CultureInfo.InvariantCulture, out var slot )\n\t\t\t\u0026\u0026 slot \u003E= 0 )\n\t\t\treturn slot;\n\n\t\treturn current;\n\t}\n}\n"},{"Ident":"pooh.geppetto","Path":"Editor/Effigy/PaintStroke.cs","FileName":"PaintStroke.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":411088,"IsPrivate":false,"Code":"using System.Collections.Generic;\n\nnamespace Effigy;\n\n/// \u003Csummary\u003E\n/// One point on a paint stroke\u0027s path: where the brush touched, and which way the surface faced\n/// there.\n///\n/// WHY THE NORMAL IS STORED BESIDE THE POSITION. A paint dab is a sphere in object space, and a\n/// sphere at a point on a thin wall reaches both sides of the wall \u2014 paint it from one side and the\n/// far face gets painted too, which reads as paint on the wrong surface rather than as thin\n/// geometry. Recording the normal lets the replay reject faces that point away from the brush, the\n/// same guard NormalBake gives the bake with its MaxDistance, and the same failure when it is\n/// skipped.\n/// \u003C/summary\u003E\npublic readonly struct PaintStrokePoint\n{\n\tpublic readonly Vec3 Position;\n\tpublic readonly Vec3 Normal;\n\n\tpublic PaintStrokePoint( Vec3 position, Vec3 normal )\n\t{\n\t\tPosition = position;\n\t\tNormal = normal;\n\t}\n}\n\n/// \u003Csummary\u003E\n/// One stroke of paint, stored as a PATH rather than as a list of dabs.\n///\n/// WHY A PATH. A dab list at ten floats a dab grows without bound and bloats the hand-written text\n/// format; a stroke stores its settings once and a handful of path points, then regenerates its dabs\n/// deterministically along the path on replay, by the same spacing rule SculptSession already uses\n/// to fill in a fast drag. That smallness is what lets paint live in the .effigy text beside the\n/// feature tree instead of in a binary side-car \u2014 and what makes it diff, which for somebody\u0027s model\n/// matters more than it sounds.\n/// \u003C/summary\u003E\npublic sealed class PaintStroke\n{\n\t/// \u003Csummary\u003EBrush colour, straight RGBA in 0..1.\u003C/summary\u003E\n\tpublic float R = 1f;\n\tpublic float G = 1f;\n\tpublic float B = 1f;\n\tpublic float A = 1f;\n\n\t/// \u003Csummary\u003EBrush radius, in world units.\u003C/summary\u003E\n\tpublic float Radius = 0.1f;\n\n\t/// \u003Csummary\u003EHow hard the stroke presses, 0..1.\u003C/summary\u003E\n\tpublic float Strength = 1f;\n\n\t/// \u003Csummary\u003EReuses \u003Csee cref=\u0022BrushFalloff\u0022/\u003E \u2014 Smooth / Linear / Sharp / Constant. Deliberately\n\t/// not a second enum: the replay weights dabs through \u003Csee cref=\u0022Brush.Falloff\u0022/\u003E and two copies\n\t/// of the same list is the thing that drifts.\u003C/summary\u003E\n\tpublic BrushFalloff Falloff = BrushFalloff.Smooth;\n\n\t/// \u003Csummary\u003EDistance between dabs along the path, as a fraction of \u003Csee cref=\u0022Radius\u0022/\u003E.\u003C/summary\u003E\n\tpublic float Spacing = 0.5f;\n\n\t/// \u003Csummary\u003E\n\t/// Whether this stroke TAKES paint away rather than laying it down.\n\t///\n\t/// AN ERASE IS A STROKE, NOT AN EDIT TO THE CANVAS. The canvas is a derived artifact replayed\n\t/// from this list on every rebuild, so an erase that scrubbed texels directly would be undone by\n\t/// the next rebuild \u2014 the paint would come back on its own, which is worse than having no eraser\n\t/// at all. Recorded here it sits in the log in the order the hand made it, and colour blending\n\t/// does not commute, so that order is the whole point.\n\t///\n\t/// THE COLOUR FIELDS ARE STILL CARRIED AND STILL MEAN NOTHING HERE. An erase subtracts its\n\t/// coverage from the canvas\u0027s alpha and never reads R/G/B. They stay rather than moving to a\n\t/// second type so that one writer and one reader serve both kinds \u2014 see StudioDocument, where an\n\t/// erase is a different LINE KIND rather than a ninth header field, because appending a field\n\t/// would shift the path every older document parses from.\n\t/// \u003C/summary\u003E\n\tpublic bool Erase;\n\n\t/// \u003Csummary\u003EThe path, in the order it was painted, each point carrying a position and the\n\t/// surface normal there. Order is the whole point: strokes are a log and colour blending does\n\t/// not commute.\u003C/summary\u003E\n\tpublic readonly List\u003CPaintStrokePoint\u003E Path = new();\n}\n"},{"Ident":"pooh.geppetto","Path":"Editor/Effigy/Rig/SkeletonRetarget.cs","FileName":"SkeletonRetarget.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":411088,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace Effigy;\n\n/// \u003Csummary\u003E\n/// Move a skinned mesh from one skeleton onto another.\n///\n/// WHAT THIS IS FOR. A playermodel is a mesh bound to CITIZEN\u0027S skeleton, in citizen\u0027s bind pose,\n/// because that is what citizen\u0027s animations were authored against and they bind by bone name. A\n/// model rigged in Effigy - or imported from anywhere else - has its own bones with its own names\n/// in its own pose. Renaming the bones is the obvious move and it is wrong: the animation supplies\n/// a rotation relative to the bind pose it was authored against, so feeding it a different bind\n/// pose bends every joint by the difference. Citizen stands in an A-pose with its arms ~40 degrees\n/// below horizontal; hand a T-posed rig citizen\u0027s walk cycle and the arms go through the floor.\n///\n/// TWO WAYS TO MAKE THE TWO AGREE, and they are not interchangeable. \u003Csee cref=\u0022To\u0022/\u003E moves the\n/// MESH onto the target\u0027s skeleton, so the model takes on the target\u0027s proportions.\n/// \u003Csee cref=\u0022Fit\u0022/\u003E moves the target\u0027s SKELETON into the mesh, so the model keeps its own. Which\n/// one a model wants depends on whether it is meant to look like itself.\n///\n/// For \u003Csee cref=\u0022To\u0022/\u003E: for each source bone this builds the transform that takes its bind pose\n/// to the target bone\u0027s bind pose, and applies that to the vertices it influences, blended by the\n/// same weights that will skin it afterwards. The result is the same mesh standing in the target\u0027s\n/// pose, bound to the target\u0027s bones - which is exactly the situation the target\u0027s animations\n/// expect. It is the operation a DCC tool calls \u0022bind to a new skeleton\u0022, written out.\n///\n/// LINEAR BLEND, AND ITS ONE KNOWN FAILURE. A vertex influenced by several bones is transformed by\n/// each and averaged. That is the same maths skinning itself uses, so the result is consistent\n/// with how the model will actually deform - and it inherits the same candy-wrapper pinching where\n/// two bones differ by a large twist. The joints that move most here are shoulders and elbows,\n/// which is where it will show first if it shows at all.\n///\n/// TWO KINDS OF CORRESPONDENCE, AND CONFLATING THEM TEARS THE MESH. Mapping \u0060upperarm_L\u0060 onto\n/// \u0060arm_upper_L\u0060 means \u0022these are the same bone\u0022: the source bone\u0027s bind position SNAPS onto the\n/// target\u0027s, which is how the model takes on the target\u0027s proportions, and is the whole point.\n/// Mapping a brow bone onto \u0060head\u0060 means something else entirely - citizen has no brow, and the\n/// geometry should simply ride along with the head where it already is. Snapping it would drag the\n/// brow into the middle of the skull. Same for a fifth finger onto a four-fingered hand.\n///\n/// So a source bone can be named as RIDE-ALONG: its weight is rebound to the target bone, but its\n/// geometry moves by the nearest true correspondence above it instead of by its own. The cost of\n/// getting this wrong is not subtle - it was long spikes radiating out of the Gearhead\u0027s face and\n/// fingers stretched onto the wrong knuckles.\n///\n/// THE MAP IS THE CALLER\u0027S PROBLEM, deliberately. Guessing that \u0060upperarm_L\u0060 means \u0060arm_upper_L\u0060\n/// is a heuristic that will be wrong on somebody\u0027s rig, silently, in a way that looks like bad\n/// weights rather than a bad guess. So the correspondence is supplied and this reports exactly\n/// what it could not place.\n/// \u003C/summary\u003E\npublic static class SkeletonRetarget\n{\n\t/// \u003Csummary\u003EWhat a retarget produced, and everything that did not survive it cleanly.\u003C/summary\u003E\n\tpublic sealed class Result\n\t{\n\t\t/// \u003Csummary\u003EThe mesh, skinned to the target\u0027s bones - moved into the target\u0027s bind pose by\n\t\t/// \u003Csee cref=\u0022To\u0022/\u003E, left exactly where it was by \u003Csee cref=\u0022Fit\u0022/\u003E.\u003C/summary\u003E\n\t\tpublic PolyMesh Mesh;\n\n\t\t/// \u003Csummary\u003EThe skeleton \u003Csee cref=\u0022Mesh\u0022/\u003E is bound to: the target itself after\n\t\t/// \u003Csee cref=\u0022To\u0022/\u003E, the target fitted into the mesh after \u003Csee cref=\u0022Fit\u0022/\u003E. Export this,\n\t\t/// not the target you passed in.\u003C/summary\u003E\n\t\tpublic Skeleton Skeleton;\n\n\t\t/// \u003Csummary\u003ESource bone names with no entry in the map, or an entry naming a bone the\n\t\t/// target does not have. Their weight had nowhere to go.\u003C/summary\u003E\n\t\tpublic List\u003Cstring\u003E Unmapped = new();\n\n\t\t/// \u003Csummary\u003EVertices that lost some weight because of \u003Csee cref=\u0022Unmapped\u0022/\u003E, and were\n\t\t/// renormalised over whatever was left.\u003C/summary\u003E\n\t\tpublic int VerticesRenormalised;\n\n\t\t/// \u003Csummary\u003EVertices with no mapped influence at all. These could not be moved and were\n\t\t/// left where they were - the one failure that shows up as geometry in the wrong\n\t\t/// place.\u003C/summary\u003E\n\t\tpublic int VerticesStranded;\n\n\t\t/// \u003Csummary\u003ETarget bones nothing was weighted onto. Expected rather than alarming: citizen\n\t\t/// carries twists, helpers and IK targets that the anim graph drives and no mesh is bound\n\t\t/// to.\u003C/summary\u003E\n\t\tpublic int TargetBonesUnused;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Retarget \u003Cparamref name=\u0022mesh\u0022/\u003E from \u003Cparamref name=\u0022from\u0022/\u003E onto \u003Cparamref name=\u0022to\u0022/\u003E.\n\t///\n\t/// \u003Cparamref name=\u0022map\u0022/\u003E is keyed by SOURCE bone name and names a target bone. Several source\n\t/// bones may name the same target - a five-fingered hand onto citizen\u0027s four is exactly that -\n\t/// and their weights are summed on the way through.\n\t///\n\t/// \u003Cparamref name=\u0022rideAlong\u0022/\u003E names source bones that have no true counterpart and should\n\t/// keep their place rather than snapping onto the bone they are bound to - see the note above.\n\t///\n\t/// \u003Cparamref name=\u0022adoptRoll\u0022/\u003E takes the target bone\u0027s frame whole instead of keeping the\n\t/// source\u0027s roll - see \u003Csee cref=\u0022Land\u0022/\u003E for why that is not the default. Kept as a switch\n\t/// because the two only differ once something animates, and the only way to know which a given\n\t/// pair of skeletons wants is to bake both and look.\n\t///\n\t/// The mesh is not modified; the result carries a clone.\n\t/// \u003C/summary\u003E\n\tpublic static Result To( PolyMesh mesh, Skeleton from, Skeleton to, IReadOnlyDictionary\u003Cstring, string\u003E map,\n\t\tIReadOnlyCollection\u003Cstring\u003E rideAlong = null, bool adoptRoll = false )\n\t{\n\t\tArgumentNullException.ThrowIfNull( mesh );\n\t\tArgumentNullException.ThrowIfNull( from );\n\t\tArgumentNullException.ThrowIfNull( to );\n\t\tArgumentNullException.ThrowIfNull( map );\n\n\t\tif ( !mesh.IsRigged )\n\t\t\tthrow new ArgumentException( \u0022The mesh has no skin weights - there is nothing to retarget.\u0022, nameof( mesh ) );\n\n\t\tvar result = new Result { Mesh = mesh.Clone(), Skeleton = to };\n\n\t\t// Per source bone: where it maps to, and the transform from its bind pose to that one.\n\t\tvar target = Resolve( from, to, map, result );\n\t\tvar delta = new Xform[from.Count];\n\n\t\tfor ( var b = 0; b \u003C from.Count; b\u002B\u002B )\n\t\t{\n\t\t\tvar t = target[b];\n\n\t\t\tif ( t \u003C 0 )\n\t\t\t\tcontinue;\n\n\t\t\t// Source bind pose to target bind pose, both in model space. Undo where the source\n\t\t\t// bone stands, then stand where the target one does - but with the source\u0027s own roll\n\t\t\t// carried across rather than the target\u0027s. See Land.\n\t\t\tdelta[b] = (adoptRoll ? to.WorldBind( t )\n\t\t\t\t: Land( from.WorldBind( b ), from.BoneDirection( b ), to.WorldBind( t ), to.BoneDirection( t ) ))\n\t\t\t\t* from.WorldBind( b ).Inverse;\n\t\t}\n\n\t\t// Ride-along bones keep the weight they were given but borrow their MOVEMENT from the\n\t\t// nearest ancestor that is a real correspondence, so their geometry stays where it sits\n\t\t// relative to that bone instead of collapsing onto the one it is now bound to. Walked\n\t\t// child-last so a chain of them (a whole pinky) all reach the same real ancestor.\n\t\tif ( rideAlong is { Count: \u003E 0 } )\n\t\t{\n\t\t\t// Its own set, case-insensitively, because the map is matched that way and a caller\n\t\t\t// handing this in as IReadOnlyCollection would otherwise get LINQ\u0027s ordinal Contains\n\t\t\t// no matter what comparer their set was built with - a silent miss that looks exactly\n\t\t\t// like a missing entry.\n\t\t\tvar passengers = new HashSet\u003Cstring\u003E( rideAlong, StringComparer.OrdinalIgnoreCase );\n\t\t\tvar passenger = new bool[from.Count];\n\n\t\t\tfor ( var b = 0; b \u003C from.Count; b\u002B\u002B )\n\t\t\t\tpassenger[b] = target[b] \u003E= 0 \u0026\u0026 passengers.Contains( from.Bones[b].Name );\n\n\t\t\tfor ( var b = 0; b \u003C from.Count; b\u002B\u002B )\n\t\t\t{\n\t\t\t\tif ( !passenger[b] )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tvar anchor = from.Bones[b].Parent;\n\n\t\t\t\twhile ( anchor \u003E= 0 \u0026\u0026 (passenger[anchor] || target[anchor] \u003C 0) )\n\t\t\t\t\tanchor = from.Bones[anchor].Parent;\n\n\t\t\t\tif ( anchor \u003E= 0 )\n\t\t\t\t\tdelta[b] = delta[anchor];\n\t\t\t\telse\n\t\t\t\t\tresult.Unmapped.Add( $\u0022{from.Bones[b].Name} rides along but has no anchored ancestor\u0022 );\n\t\t\t}\n\t\t}\n\n\t\tRebind( result, from.Count, to.Count, target, delta );\n\t\treturn result;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The other way round from \u003Csee cref=\u0022To\u0022/\u003E: move the TARGET skeleton into the mesh and leave\n\t/// the mesh exactly where it is.\n\t///\n\t/// WHY THIS EXISTS. \u003Csee cref=\u0022To\u0022/\u003E snaps every joint of the mesh onto the target\u0027s joints, so\n\t/// the model takes on the target\u0027s proportions. On a body that is a different shape that is a\n\t/// mangle: each bone\u0027s geometry moves rigidly to where the target\u0027s bone starts - citizen\u0027s\n\t/// upper arm is 10in against the Gearhead\u0027s 7.7 - and the skin bridges every gap by stretching.\n\t/// The Gearhead came out long-limbed and pinched at the joints with nothing wrong in its\n\t/// weights.\n\t///\n\t/// So here the bones move instead. Every target bone with a source counterpart stands on that\n\t/// bone\u0027s head; the ones with none - twists, helpers, IK - ride their parent, turned and\n\t/// stretched with it. Each bone keeps the TARGET\u0027s frame convention, swung by the shortest arc\n\t/// onto the direction the source limb actually runs: citizen\u0027s \u002BX still runs down the bone and\n\t/// its roll is still citizen\u0027s. That is what lets citizen\u0027s clips drive it. A clip is local\n\t/// rotations in citizen\u0027s convention, so a bone whose axes mean what citizen\u0027s mean ends up\n\t/// pointing where citizen\u0027s would, and the mesh hanging off it follows. A T-posed arm on an\n\t/// A-posed skeleton is just a larger swing at the shoulder.\n\t///\n\t/// WHICH WAY A BONE RUNS is named by \u003Cparamref name=\u0022aims\u0022/\u003E - target bone name to the target\n\t/// bones it points at, averaged - not guessed. \u0022Furthest child\u0022 is wrong on citizen twice over:\n\t/// spine_2\u0027s children include both clavicles, and the hand\u0027s furthest child is an IK rule bone\n\t/// forty inches away. A bone with no aim, or none that landed anywhere, keeps its parent\u0027s\n\t/// swing, which is right for a head, a fingertip, or anything rigid on its parent.\n\t///\n\t/// THE CLIPS STILL CARRY CITIZEN\u0027S LENGTHS. Local translation is part of every clip, and a\n\t/// fitted bone played at citizen\u0027s translations is pulled straight back to citizen\u0027s\n\t/// proportions. The model that wears this must mark those bones ignore_Translation - see\n\t/// \u003Csee cref=\u0022CitizenBoneMap.KeepsOwnLength\u0022/\u003E.\n\t///\n\t/// Weights are rebound by the same map as \u003Csee cref=\u0022To\u0022/\u003E. Ride-along bones place a target\n\t/// bone only where no true correspondence does - the Gearhead\u0027s lids hinge where its own eyelid\n\t/// bones are because nothing else claims citizen\u0027s.\n\t/// \u003C/summary\u003E\n\tpublic static Result Fit( PolyMesh mesh, Skeleton from, Skeleton to, IReadOnlyDictionary\u003Cstring, string\u003E map,\n\t\tIReadOnlyCollection\u003Cstring\u003E rideAlong, IReadOnlyDictionary\u003Cstring, string[]\u003E aims )\n\t{\n\t\tArgumentNullException.ThrowIfNull( mesh );\n\t\tArgumentNullException.ThrowIfNull( from );\n\t\tArgumentNullException.ThrowIfNull( to );\n\t\tArgumentNullException.ThrowIfNull( map );\n\t\tArgumentNullException.ThrowIfNull( aims );\n\n\t\tif ( !mesh.IsRigged )\n\t\t\tthrow new ArgumentException( \u0022The mesh has no skin weights - there is nothing to retarget.\u0022, nameof( mesh ) );\n\n\t\tvar result = new Result { Mesh = mesh.Clone() };\n\t\tvar target = Resolve( from, to, map, result );\n\t\tvar passengers = new HashSet\u003Cstring\u003E( rideAlong ?? Array.Empty\u003Cstring\u003E(), StringComparer.OrdinalIgnoreCase );\n\n\t\t// Which source bone\u0027s head each target bone stands on. True correspondences first, then\n\t\t// passengers for whatever is still free; within each, the bone nearest the root wins, so\n\t\t// \u0060chest\u0060 and \u0060spine_03\u0060 both naming spine_2 cannot fight over it.\n\t\tvar placedBy = new int[to.Count];\n\t\tArray.Fill( placedBy, -1 );\n\n\t\tforeach ( var passes in new[] { false, true } )\n\t\t{\n\t\t\tfor ( var b = 0; b \u003C from.Count; b\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar t = target[b];\n\n\t\t\t\tif ( t \u003E= 0 \u0026\u0026 placedBy[t] \u003C 0 \u0026\u0026 passengers.Contains( from.Bones[b].Name ) == passes )\n\t\t\t\t\tplacedBy[t] = b;\n\t\t\t}\n\t\t}\n\n\t\tvar bind = new Xform[to.Count];\n\t\tvar at = new Vec3[to.Count];\n\n\t\tfor ( var t = 0; t \u003C to.Count; t\u002B\u002B )\n\t\t{\n\t\t\tbind[t] = to.WorldBind( t );\n\n\t\t\tif ( placedBy[t] \u003E= 0 )\n\t\t\t\tat[t] = from.WorldBind( placedBy[t] ).Origin;\n\t\t}\n\n\t\t// Parents before children, which the skeleton guarantees: an unplaced bone needs its\n\t\t// parent\u0027s final swing and stretch, and an aim only ever reads bones placed up front.\n\t\tvar swing = new Xform[to.Count];\n\t\tvar stretch = new float[to.Count];\n\t\tvar fitted = new Xform[to.Count];\n\n\t\tfor ( var t = 0; t \u003C to.Count; t\u002B\u002B )\n\t\t{\n\t\t\tvar parent = to.Bones[t].Parent;\n\n\t\t\tswing[t] = parent \u003C 0 ? Xform.Identity : swing[parent];\n\t\t\tstretch[t] = parent \u003C 0 ? 1f : stretch[parent];\n\n\t\t\t// Nothing stands here: carry the target\u0027s own offset from the parent, turned and\n\t\t\t// scaled the way the parent was, so a twist halfway down a limb stays halfway down it.\n\t\t\tif ( placedBy[t] \u003C 0 )\n\t\t\t{\n\t\t\t\tat[t] = parent \u003C 0 ? bind[t].Origin\n\t\t\t\t\t: at[parent] \u002B swing[parent].TransformDirection( bind[t].Origin - bind[parent].Origin ) * stretch[parent];\n\t\t\t}\n\n\t\t\tif ( aims.TryGetValue( to.Bones[t].Name, out var aimNames ) )\n\t\t\t{\n\t\t\t\tvar landed = aimNames.Select( to.IndexOf ).Where( a =\u003E a \u003E= 0 \u0026\u0026 placedBy[a] \u003E= 0 ).ToList();\n\n\t\t\t\tif ( landed.Count \u003E 0 )\n\t\t\t\t{\n\t\t\t\t\tvar was = Centroid( landed.Select( a =\u003E bind[a].Origin ) ) - bind[t].Origin;\n\t\t\t\t\tvar now = Centroid( landed.Select( a =\u003E at[a] ) ) - at[t];\n\n\t\t\t\t\tif ( was.Length \u003E 1e-4f \u0026\u0026 now.Length \u003E 1e-4f )\n\t\t\t\t\t{\n\t\t\t\t\t\tswing[t] = Arc( was.Normal, now.Normal );\n\t\t\t\t\t\tstretch[t] = now.Length / was.Length;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar turned = swing[t] * new Xform( bind[t].X, bind[t].Y, bind[t].Z, Vec3.Zero );\n\t\t\tfitted[t] = new Xform( turned.X, turned.Y, turned.Z, at[t] );\n\t\t}\n\n\t\tvar skeleton = to.Clone();\n\n\t\tfor ( var t = 0; t \u003C to.Count; t\u002B\u002B )\n\t\t{\n\t\t\tvar parent = to.Bones[t].Parent;\n\t\t\tskeleton.Bones[t].Local = parent \u003C 0 ? fitted[t] : fitted[parent].Inverse * fitted[t];\n\t\t}\n\n\t\tresult.Skeleton = skeleton;\n\n\t\tRebind( result, from.Count, to.Count, target, delta: null );\n\t\treturn result;\n\t}\n\n\t/// \u003Csummary\u003EWhich target bone each source bone\u0027s weight goes to, or -1, with every miss\n\t/// recorded on \u003Cparamref name=\u0022result\u0022/\u003E.\u003C/summary\u003E\n\tstatic int[] Resolve( Skeleton from, Skeleton to, IReadOnlyDictionary\u003Cstring, string\u003E map, Result result )\n\t{\n\t\tvar target = new int[from.Count];\n\n\t\tfor ( var b = 0; b \u003C from.Count; b\u002B\u002B )\n\t\t{\n\t\t\tvar name = from.Bones[b].Name;\n\n\t\t\tif ( !map.TryGetValue( name, out var wanted ) || string.IsNullOrEmpty( wanted ) )\n\t\t\t{\n\t\t\t\ttarget[b] = -1;\n\t\t\t\tresult.Unmapped.Add( name );\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tvar t = to.IndexOf( wanted );\n\n\t\t\tif ( t \u003C 0 )\n\t\t\t{\n\t\t\t\ttarget[b] = -1;\n\t\t\t\tresult.Unmapped.Add( $\u0022{name} -\u003E {wanted} (the target has no such bone)\u0022 );\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\ttarget[b] = t;\n\t\t}\n\n\t\treturn target;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Put every vertex\u0027s weight onto the target bones, and move it by \u003Cparamref name=\u0022delta\u0022/\u003E\n\t/// if there is one - \u003Csee cref=\u0022To\u0022/\u003E moves the mesh, \u003Csee cref=\u0022Fit\u0022/\u003E passes null and only\n\t/// renames what each vertex hangs off.\n\t/// \u003C/summary\u003E\n\tstatic void Rebind( Result result, int fromCount, int toCount, int[] target, Xform[] delta )\n\t{\n\t\tvar used = new bool[toCount];\n\t\tvar skin = result.Mesh.Skin;\n\n\t\tfor ( var v = 0; v \u003C result.Mesh.VertexCount; v\u002B\u002B )\n\t\t{\n\t\t\tvar weights = skin[v];\n\t\t\tvar kept = 0f;\n\n\t\t\tforeach ( var w in weights )\n\t\t\t{\n\t\t\t\tif ( w.Bone \u003E= 0 \u0026\u0026 w.Bone \u003C fromCount \u0026\u0026 target[w.Bone] \u003E= 0 )\n\t\t\t\t\tkept \u002B= w.Weight;\n\t\t\t}\n\n\t\t\tif ( kept \u003C= 1e-6f )\n\t\t\t{\n\t\t\t\t// Nothing this vertex is attached to went anywhere. Moving it by an arbitrary bone\n\t\t\t\t// would be a guess; leaving it is at least honest, and the count says how bad it is.\n\t\t\t\tresult.VerticesStranded\u002B\u002B;\n\t\t\t\tskin[v] = Array.Empty\u003CBoneWeight\u003E();\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ( kept \u003C 0.999f )\n\t\t\t\tresult.VerticesRenormalised\u002B\u002B;\n\n\t\t\t// The vertex goes where its bones take it, averaged - the same blend that will skin it.\n\t\t\tvar p = result.Mesh.Positions[v];\n\t\t\tvar moved = Vec3.Zero;\n\t\t\tvar merged = new Dictionary\u003Cint, float\u003E();\n\n\t\t\tforeach ( var w in weights )\n\t\t\t{\n\t\t\t\tif ( w.Bone \u003C 0 || w.Bone \u003E= fromCount || target[w.Bone] \u003C 0 )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tvar share = w.Weight / kept;\n\n\t\t\t\tif ( delta is not null )\n\t\t\t\t\tmoved \u002B= delta[w.Bone].TransformPoint( p ) * share;\n\n\t\t\t\tvar t = target[w.Bone];\n\t\t\t\tmerged[t] = merged.TryGetValue( t, out var had ) ? had \u002B share : share;\n\t\t\t\tused[t] = true;\n\t\t\t}\n\n\t\t\tif ( delta is not null )\n\t\t\t\tresult.Mesh.Positions[v] = moved;\n\n\t\t\tskin[v] = merged.Select( kv =\u003E new BoneWeight( kv.Key, kv.Value ) )\n\t\t\t\t.OrderByDescending( w =\u003E w.Weight )\n\t\t\t\t.ToArray();\n\t\t}\n\n\t\tresult.TargetBonesUnused = used.Count( u =\u003E !u );\n\t\tresult.Unmapped = result.Unmapped.Distinct().ToList();\n\t}\n\n\tstatic Vec3 Centroid( IEnumerable\u003CVec3\u003E points )\n\t{\n\t\tvar sum = Vec3.Zero;\n\t\tvar n = 0;\n\n\t\tforeach ( var p in points )\n\t\t{\n\t\t\tsum \u002B= p;\n\t\t\tn\u002B\u002B;\n\t\t}\n\n\t\treturn n == 0 ? sum : sum / n;\n\t}\n\n\t/// \u003Csummary\u003EThe shortest rotation taking unit \u003Cparamref name=\u0022from\u0022/\u003E onto unit\n\t/// \u003Cparamref name=\u0022to\u0022/\u003E. Antiparallel has no shortest one, so it turns about any fixed\n\t/// perpendicular - stable, and no worse than every other choice.\u003C/summary\u003E\n\tstatic Xform Arc( Vec3 from, Vec3 to )\n\t{\n\t\tvar axis = Vec3.Cross( from, to );\n\t\tvar sin = axis.Length;\n\t\tvar cos = Vec3.Dot( from, to );\n\n\t\tif ( sin \u003E= 1e-6f )\n\t\t\treturn Xform.Rotate( axis, MathF.Atan2( sin, cos ) );\n\n\t\tif ( cos \u003E 0f )\n\t\t\treturn Xform.Identity;\n\n\t\tvar seed = MathF.Abs( from.x ) \u003C 0.9f ? new Vec3( 1, 0, 0 ) : new Vec3( 0, 1, 0 );\n\t\treturn Xform.Rotate( Vec3.Cross( from, seed ), MathF.PI );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Where a source bone should stand on the target skeleton: the target bone\u0027s position and\n\t/// direction, and the SOURCE bone\u0027s roll about that direction.\n\t///\n\t/// WHY NOT JUST TAKE THE TARGET\u0027S FRAME. Aiming a bone head to tail pins two of its axes and\n\t/// leaves it free to spin about its own length, so every rig picks that roll by a convention\n\t/// of its own - citizen\u0027s came out of its fbx, a bone built by \u003Csee cref=\u0022Skeleton.AddBone\u0022/\u003E\n\t/// from head and tail points gets whatever \u003Cc\u003EPerpendicularAxes\u003C/c\u003E seeded. Two skeletons\n\t/// agreeing perfectly on where every joint is can still disagree about roll on every single\n\t/// bone, by any angle up to 180 degrees. Adopting the target\u0027s roll then spins each limb\u0027s\n\t/// geometry about its own length by a different arbitrary amount, which does not look like a\n\t/// convention mismatch - it looks like the mesh has been through a shredder.\n\t///\n\t/// So the twist is dropped and only the swing is kept: the shortest rotation that takes the\n\t/// source bone\u0027s direction onto the target\u0027s, applied to the source\u0027s own cross-axes. Nothing\n\t/// is lost by it. The skin binds against the target\u0027s real bind pose either way, and at rest\n\t/// pose equals bind, so the vertex stays exactly where this puts it - the delta is ours to\n\t/// choose, and the choice that moves geometry least is the right one.\n\t///\n\t/// DIRECTION COMES FROM GEOMETRY, NOT THE FRAME. \u003Cparamref name=\u0022sourceDir\u0022/\u003E and\n\t/// \u003Cparamref name=\u0022targetDir\u0022/\u003E are the bones\u0027 head-to-child directions, not their \u002BY axes -\n\t/// a frame\u0027s \u002BY is only the bone direction for an Effigy-built skeleton, and citizen is\n\t/// \u002BX-down-the-bone. Swinging .Y onto .Y across that convention boundary spins the mesh a\n\t/// quarter turn about every limb.\n\t/// \u003C/summary\u003E\n\tstatic Xform Land( Xform source, Vec3 sourceDir, Xform target, Vec3 targetDir )\n\t{\n\t\tvar from = sourceDir.Normal;\n\t\tvar to = targetDir.Normal;\n\n\t\tvar axis = Vec3.Cross( from, to );\n\t\tvar sin = axis.Length;\n\t\tvar cos = Vec3.Dot( from, to );\n\n\t\t// Antiparallel has no shortest rotation - every half turn about a perpendicular axis is\n\t\t// equally short - so take the one about the source\u0027s own X, which is perpendicular by\n\t\t// construction and keeps the choice stable.\n\t\tvar swing = sin \u003C 1e-6f\n\t\t\t? (cos \u003E 0f ? Xform.Identity : Xform.Rotate( source.X, MathF.PI ))\n\t\t\t: Xform.Rotate( axis, MathF.Atan2( sin, cos ) );\n\n\t\t// Swing the WHOLE source frame, not just its cross-axes with the target direction bolted\n\t\t// into the \u002BY slot. Slotting the direction into \u002BY assumed the target\u0027s bone direction IS\n\t\t// its \u002BY, which is true of an Effigy skeleton and false of citizen\u0027s \u002BX-down-the-bone - on\n\t\t// the identity retarget that single-slot form stopped being the source frame and moved the\n\t\t// mesh. Applying the swing to every axis keeps the source\u0027s roll and collapses to the\n\t\t// source frame (and so an identity delta) exactly when the two directions already agree.\n\t\treturn new Xform(\n\t\t\tswing.TransformDirection( source.X.Normal ),\n\t\t\tswing.TransformDirection( source.Y.Normal ),\n\t\t\tswing.TransformDirection( source.Z.Normal ),\n\t\t\ttarget.Origin );\n\t}\n}\n"},{"Ident":"pooh.geppetto","Path":"Editor/Effigy/Script/BuildScript.cs","FileName":"BuildScript.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":411088,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.IO;\nusing System.Linq;\nusing System.Reflection;\nusing System.Text;\nusing Effigy.Render;\n\nnamespace Effigy;\n\n/// \u003Csummary\u003E\n/// A build script: what to add to the studio, in order, in one text file that runs headlessly\n/// and in the editor alike.\n///\n///   # a comment\n///   add Primitive name=Torso shape=Box sizex=20 sizey=12 sizez=30 position=@ground\u002B0,0,15\n///   add Part name=Buckle part=Buckle size=6 length=4 position=@Torso.front\u002B1,0,-6\n///   add Spline name=Cable points=@Torso.back;-10,4,20;-14,0,8 radius=0.8\n///   add CurveDeform bodies=Tail\n///   set Torso sizez=32\n///   fix Torso all\n///   render out/torso.png view=sheet\n///   describe\n///   match front reference.png out/match.png\n///\n/// \u0060add TYPE\u0060 takes a feature type (Primitive, Spline, Part, Profile, Transform, Mirror,\n/// Subdivide\u2026) and \u0060key=value\u0060 pairs naming its parameters by label or field. Anywhere a point\n/// goes, \u0060@landmark\u0060 or \u0060@landmark\u002Bdx,dy,dz\u0060 names a place on the model \u2014 a body\u0027s side, a\n/// bone, the ground \u2014 resolved by \u003Csee cref=\u0022ModelMeasure.TryLandmark\u0022/\u003E against the model AS\n/// BUILT SO FAR, so a part can sit on the body the line before it made.\n///\n/// Every command runs a rebuild when it changes the model, and a feature that fails stops the\n/// script with the feature\u0027s own diagnostic \u2014 its cause and its remedies \u2014 so what comes back\n/// is what to change, not a stack trace.\n/// \u003C/summary\u003E\npublic static class BuildScript\n{\n\tpublic sealed class Result\n\t{\n\t\tpublic bool Ok = true;\n\t\tpublic List\u003Cstring\u003E Log = new();\n\t\tpublic List\u003Cstring\u003E Written = new();\n\t\tpublic string Error;\n\t\tpublic int ErrorLine;\n\n\t\t/// \u003Csummary\u003EWhere each body turns or stands, by body name, set with \u0060pivot\u0060. Export moves the\n\t\t/// body so this point is its model origin, and the layout says where that point was.\u003C/summary\u003E\n\t\tpublic Dictionary\u003Cstring, Vec3\u003E Pivots = new( StringComparer.OrdinalIgnoreCase );\n\n\t\tpublic override string ToString()\n\t\t{\n\t\t\tvar sb = new StringBuilder();\n\t\t\tforeach ( var line in Log ) sb.AppendLine( line );\n\t\t\tif ( Error is not null ) sb.AppendLine( $\u0022ERROR line {ErrorLine}: {Error}\u0022 );\n\t\t\treturn sb.ToString().TrimEnd();\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003ERun \u003Cparamref name=\u0022text\u0022/\u003E against \u003Cparamref name=\u0022studio\u0022/\u003E. Files a \u0060render\u0060,\n\t/// \u0060match\u0060 or \u0060save\u0060 writes go under \u003Cparamref name=\u0022baseDir\u0022/\u003E when their path is relative.\u003C/summary\u003E\n\tpublic static Result Run( PartStudio studio, string text, string baseDir = null )\n\t{\n\t\tvar result = new Result();\n\t\tbaseDir ??= Directory.GetCurrentDirectory();\n\n\t\tif ( studio is null )\n\t\t{\n\t\t\tresult.Ok = false;\n\t\t\tresult.Error = \u0022No studio.\u0022;\n\t\t\treturn result;\n\t\t}\n\n\t\tvar lines = (text ?? \u0022\u0022).Replace( \u0022\\r\\n\u0022, \u0022\\n\u0022 ).Split( \u0027\\n\u0027 );\n\n\t\tfor ( var i = 0; i \u003C lines.Length; i\u002B\u002B )\n\t\t{\n\t\t\tvar line = lines[i].Trim();\n\n\t\t\tif ( line.Length == 0 || line.StartsWith( \u0027#\u0027 ) )\n\t\t\t\tcontinue;\n\n\t\t\ttry\n\t\t\t{\n\t\t\t\tif ( !Command( studio, Tokenise( line ), baseDir, result ) )\n\t\t\t\t{\n\t\t\t\t\tresult.Ok = false;\n\t\t\t\t\tresult.ErrorLine = i \u002B 1;\n\t\t\t\t\treturn result;\n\t\t\t\t}\n\t\t\t}\n\t\t\tcatch ( Exception e )\n\t\t\t{\n\t\t\t\tresult.Ok = false;\n\t\t\t\tresult.ErrorLine = i \u002B 1;\n\t\t\t\tresult.Error = e.Message;\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tstatic bool Command( PartStudio studio, List\u003Cstring\u003E tokens, string baseDir, Result r )\n\t{\n\t\tif ( tokens.Count == 0 )\n\t\t\treturn true;\n\n\t\tvar verb = tokens[0].ToLowerInvariant();\n\t\tvar args = tokens.Skip( 1 ).ToList();\n\n\t\tswitch ( verb )\n\t\t{\n\t\t\tcase \u0022add\u0022:\n\t\t\t{\n\t\t\t\tif ( args.Count == 0 )\n\t\t\t\t\treturn Fail( r, \u0022add needs a feature type: add Primitive shape=Box \u2026\u0022 );\n\n\t\t\t\tvar feature = Create( args[0] );\n\n\t\t\t\tif ( feature is null )\n\t\t\t\t\treturn Fail( r, $\u0022No feature type \u0027{args[0]}\u0027. Types: {string.Join( \u0022, \u0022, FeatureTypeNames() )}.\u0022 );\n\n\t\t\t\tvar pairs = Pairs( args.Skip( 1 ) );\n\n\t\t\t\tif ( !Assign( studio, feature, pairs, r ) )\n\t\t\t\t\treturn false;\n\n\t\t\t\tstudio.Add( feature );\n\t\t\t\treturn RebuildAndCheck( studio, feature, r, $\u0022added {feature.TypeName} \u0027{feature.Name ?? feature.Id}\u0027\u0022 );\n\t\t\t}\n\n\t\t\tcase \u0022set\u0022:\n\t\t\t{\n\t\t\t\tif ( args.Count \u003C 2 )\n\t\t\t\t\treturn Fail( r, \u0022set needs a feature name and key=value pairs.\u0022 );\n\n\t\t\t\tvar feature = FindFeature( studio, args[0] );\n\n\t\t\t\tif ( feature is null )\n\t\t\t\t\treturn Fail( r, $\u0022No feature called \u0027{args[0]}\u0027. Features: {string.Join( \u0022, \u0022, studio.Features.Select( f =\u003E f.Name ?? f.TypeName ) )}.\u0022 );\n\n\t\t\t\tif ( !Assign( studio, feature, Pairs( args.Skip( 1 ) ), r ) )\n\t\t\t\t\treturn false;\n\n\t\t\t\tstudio.MarkDirty( feature );\n\t\t\t\treturn RebuildAndCheck( studio, feature, r, $\u0022set {feature.Name ?? feature.TypeName}\u0022 );\n\t\t\t}\n\n\t\t\tcase \u0022remove\u0022:\n\t\t\t{\n\t\t\t\tif ( args.Count \u003C 1 )\n\t\t\t\t\treturn Fail( r, \u0022remove needs a feature name.\u0022 );\n\n\t\t\t\tvar feature = FindFeature( studio, args[0] );\n\n\t\t\t\tif ( feature is null )\n\t\t\t\t\treturn Fail( r, $\u0022No feature called \u0027{args[0]}\u0027.\u0022 );\n\n\t\t\t\tstudio.Remove( feature );\n\t\t\t\tstudio.Rebuild();\n\t\t\t\tr.Log.Add( $\u0022removed {feature.Name ?? feature.TypeName}\u0022 );\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tcase \u0022rebuild\u0022:\n\t\t\t\tstudio.Rebuild();\n\t\t\t\tr.Log.Add( \u0022rebuilt\u0022 );\n\t\t\t\treturn true;\n\n\t\t\tcase \u0022fix\u0022:\n\t\t\t{\n\t\t\t\tif ( args.Count \u003C 2 )\n\t\t\t\t\treturn Fail( r, \u0022fix needs a body (or all) and the fixes: fix Torso loose holes normals\u0022 );\n\n\t\t\t\tvar targets = args[0].Equals( \u0022all\u0022, StringComparison.OrdinalIgnoreCase )\n\t\t\t\t\t? studio.Bodies.ToList()\n\t\t\t\t\t: new List\u003CBody\u003E { ModelMeasure.FindBody( studio, args[0] ) };\n\n\t\t\t\tif ( targets.Count == 0 || targets[0] is null )\n\t\t\t\t\treturn Fail( r, $\u0022No body called \u0027{args[0]}\u0027. Bodies: {string.Join( \u0022, \u0022, studio.Bodies.Select( b =\u003E b.Name ?? b.Id ) )}.\u0022 );\n\n\t\t\t\tforeach ( var body in targets )\n\t\t\t\t\tforeach ( var line in MeshFix.Apply( body.Mesh, args.Skip( 1 ) ) )\n\t\t\t\t\t\tr.Log.Add( $\u0022{body.Name ?? body.Id} {line}\u0022 );\n\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tcase \u0022render\u0022:\n\t\t\t{\n\t\t\t\tif ( args.Count \u003C 1 )\n\t\t\t\t\treturn Fail( r, \u0022render needs an output path: render out/model.png view=sheet\u0022 );\n\n\t\t\t\tvar path = Resolve( baseDir, args[0] );\n\t\t\t\tvar opts = Pairs( args.Skip( 1 ) );\n\t\t\t\tvar viewName = opts.GetValueOrDefault( \u0022view\u0022, \u0022sheet\u0022 );\n\t\t\t\tvar size = (int)Number( opts.GetValueOrDefault( \u0022size\u0022, \u0022512\u0022 ) );\n\t\t\t\tvar options = new RenderOptions\n\t\t\t\t{\n\t\t\t\t\tWireframe = Truthy( opts.GetValueOrDefault( \u0022wire\u0022, \u00221\u0022 ) ),\n\t\t\t\t\tBones = Truthy( opts.GetValueOrDefault( \u0022bones\u0022, \u00220\u0022 ) ),\n\t\t\t\t\tLabels = Truthy( opts.GetValueOrDefault( \u0022labels\u0022, \u00221\u0022 ) ),\n\t\t\t\t\tGrid = Truthy( opts.GetValueOrDefault( \u0022grid\u0022, \u00221\u0022 ) ),\n\t\t\t\t\tFocusBodyId = opts.TryGetValue( \u0022focus\u0022, out var focus ) ? ModelMeasure.FindBody( studio, focus )?.Id : null,\n\t\t\t\t};\n\n\t\t\t\tDirectory.CreateDirectory( Path.GetDirectoryName( Path.GetFullPath( path ) ) ?? \u0022.\u0022 );\n\n\t\t\t\tModelRender.Named( studio.Bodies, viewName, size, options, studio.Rig ).SavePng( path );\n\n\t\t\t\tr.Written.Add( path );\n\t\t\t\tr.Log.Add( $\u0022rendered {viewName} to {path}\u0022 );\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tcase \u0022describe\u0022:\n\t\t\t\tr.Log.Add( ModelMeasure.Describe( studio ) );\n\t\t\t\treturn true;\n\n\t\t\tcase \u0022measure\u0022:\n\t\t\tcase \u0022at\u0022:\n\t\t\t{\n\t\t\t\tif ( args.Count \u003C 1 )\n\t\t\t\t\treturn Fail( r, \u0022measure needs a landmark or a point: measure Torso.top\u0022 );\n\n\t\t\t\tif ( !Point( studio, args[0], out var p, out var why ) )\n\t\t\t\t\treturn Fail( r, why );\n\n\t\t\t\tr.Log.Add( ModelMeasure.At( studio, p ) );\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tcase \u0022distance\u0022:\n\t\t\t{\n\t\t\t\tif ( args.Count \u003C 2 )\n\t\t\t\t\treturn Fail( r, \u0022distance needs two landmarks: distance Hand_L Hand_R\u0022 );\n\n\t\t\t\tif ( !Point( studio, args[0], out var a, out var whyA ) ) return Fail( r, whyA );\n\t\t\t\tif ( !Point( studio, args[1], out var b, out var whyB ) ) return Fail( r, whyB );\n\n\t\t\t\tvar d = b - a;\n\t\t\t\tr.Log.Add( $\u0022{args[0]} to {args[1]}: {d.Length:0.###} (dx {d.x:0.###}, dy {d.y:0.###}, dz {d.z:0.###})\u0022 );\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tcase \u0022symmetry\u0022:\n\t\t\t{\n\t\t\t\tforeach ( var body in args.Count \u003E 0 ? new[] { ModelMeasure.FindBody( studio, args[0] ) } : studio.Bodies.ToArray() )\n\t\t\t\t{\n\t\t\t\t\tif ( body?.Mesh is null ) continue;\n\t\t\t\t\tr.Log.Add( $\u0022{body.Name ?? body.Id}: left-right symmetry error {ModelMeasure.SymmetryError( body.Mesh ) * 100f:0.#}% of width\u0022 );\n\t\t\t\t}\n\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tcase \u0022match\u0022:\n\t\t\t{\n\t\t\t\tif ( args.Count \u003C 2 )\n\t\t\t\t\treturn Fail( r, \u0022match needs a view and a reference PNG: match front ref.png [out.png]\u0022 );\n\n\t\t\t\tvar view = RenderView.Named( args[0] );\n\n\t\t\t\tif ( view is null )\n\t\t\t\t\treturn Fail( r, $\u0022No view called \u0027{args[0]}\u0027.\u0022 );\n\n\t\t\t\tvar refPath = Resolve( baseDir, args[1] );\n\n\t\t\t\tif ( !File.Exists( refPath ) )\n\t\t\t\t\treturn Fail( r, $\u0022No reference image at {refPath}.\u0022 );\n\n\t\t\t\tconst int size = 256;\n\t\t\t\tvar (w, h, rgb, alpha) = Raster.LoadPng( refPath );\n\t\t\t\tvar reference = ReferenceMatch.ReferenceSilhouette( w, h, rgb, alpha, size, out var aspect );\n\t\t\t\tvar match = ReferenceMatch.Compare( studio.Bodies, view, reference, aspect, size );\n\n\t\t\t\tr.Log.Add( match.Report );\n\n\t\t\t\tif ( args.Count \u003E 2 )\n\t\t\t\t{\n\t\t\t\t\tvar outPath = Resolve( baseDir, args[2] );\n\t\t\t\t\tDirectory.CreateDirectory( Path.GetDirectoryName( Path.GetFullPath( outPath ) ) ?? \u0022.\u0022 );\n\t\t\t\t\tmatch.Diff.SavePng( outPath );\n\t\t\t\t\tr.Written.Add( outPath );\n\t\t\t\t}\n\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tcase \u0022save\u0022:\n\t\t\t{\n\t\t\t\tif ( args.Count \u003C 1 )\n\t\t\t\t\treturn Fail( r, \u0022save needs a path.\u0022 );\n\n\t\t\t\tvar path = Resolve( baseDir, args[0] );\n\t\t\t\tDirectory.CreateDirectory( Path.GetDirectoryName( Path.GetFullPath( path ) ) ?? \u0022.\u0022 );\n\t\t\t\tStudioDocument.WriteFile( studio, path );\n\t\t\t\tr.Written.Add( path );\n\t\t\t\tr.Log.Add( $\u0022saved {path}\u0022 );\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tcase \u0022echo\u0022:\n\t\t\t\tr.Log.Add( string.Join( \u0022 \u0022, args ) );\n\t\t\t\treturn true;\n\n\t\t\tcase \u0022material\u0022:\n\t\t\t{\n\t\t\t\t// material 1 materials/alley/brass.vmat \u2014 what slot 1 is exported as.\n\t\t\t\tif ( args.Count \u003C 2 || !int.TryParse( args[0], out var slot ) )\n\t\t\t\t\treturn Fail( r, \u0022material needs a slot number and a .vmat path: material 1 materials/alley/brass.vmat\u0022 );\n\n\t\t\t\tstudio.MaterialNames[slot] = args[1];\n\t\t\t\tstudio.MarkDirty( 0 );\n\t\t\t\tr.Log.Add( $\u0022slot {slot} is {args[1]}\u0022 );\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tcase \u0022pivot\u0022:\n\t\t\t{\n\t\t\t\t// pivot Bob3 @Beam.bottom \u2014 the point this part turns about, which export makes its origin.\n\t\t\t\tif ( args.Count \u003C 2 )\n\t\t\t\t\treturn Fail( r, \u0022pivot needs a body and a point: pivot Arm @Hub.centre\u0022 );\n\n\t\t\t\tif ( ModelMeasure.FindBody( studio, args[0] ) is null )\n\t\t\t\t\treturn Fail( r, $\u0022No body called \u0027{args[0]}\u0027.\u0022 );\n\n\t\t\t\tif ( !Point( studio, args[1], out var p, out var why ) )\n\t\t\t\t\treturn Fail( r, why );\n\n\t\t\t\tr.Pivots[args[0]] = p;\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tcase \u0022export\u0022:\n\t\t\t{\n\t\t\t\t// export models/civic_art/orrery split=1 assets=C:/proj/Assets name=orrery\n\t\t\t\tif ( args.Count \u003C 1 )\n\t\t\t\t\treturn Fail( r, \u0022export needs a folder under the assets root: export models/civic_art/orrery [split=1] [assets=root] [name=x]\u0022 );\n\n\t\t\t\tvar folder = args[0].Replace( \u0027\\\\\u0027, \u0027/\u0027 ).Trim( \u0027/\u0027 );\n\t\t\t\tvar opts = Pairs( args.Skip( 1 ) );\n\t\t\t\tvar root = opts.TryGetValue( \u0022assets\u0022, out var assets ) ? assets : baseDir;\n\t\t\t\tvar split = Truthy( opts.GetValueOrDefault( \u0022split\u0022, \u00220\u0022 ) );\n\t\t\t\tvar name = opts.GetValueOrDefault( \u0022name\u0022, folder.Contains( \u0027/\u0027 ) ? folder[(folder.LastIndexOf( \u0027/\u0027 ) \u002B 1)..] : folder );\n\t\t\t\tvar dir = Path.Combine( root, folder );\n\t\t\t\tDirectory.CreateDirectory( dir );\n\n\t\t\t\tvar layout = new StringBuilder();\n\t\t\t\tlayout.AppendLine( \u0022# part\\tmodel\\tpivot x\\ty\\tz\\tsize x\\ty\\tz\u0022 );\n\n\t\t\t\tvar bodies = studio.Bodies.Where( b =\u003E b?.Mesh is not null \u0026\u0026 b.Mesh.Faces.Count \u003E 0 \u0026\u0026 b.Visible ).ToList();\n\n\t\t\t\tif ( bodies.Count == 0 )\n\t\t\t\t\treturn Fail( r, \u0022Nothing to export: no visible bodies.\u0022 );\n\n\t\t\t\tif ( split )\n\t\t\t\t{\n\t\t\t\t\tforeach ( var body in bodies )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar partName = Safe( body.Name ?? body.Id );\n\t\t\t\t\t\tvar mesh = body.Mesh.Clone();\n\t\t\t\t\t\tvar (min, max) = ModelMeasure.Bounds( mesh );\n\t\t\t\t\t\tvar pivot = r.Pivots.TryGetValue( body.Name ?? \u0022\u0022, out var p ) ? p : new Vec3( (min.x \u002B max.x) * 0.5f, (min.y \u002B max.y) * 0.5f, min.z );\n\n\t\t\t\t\t\tMeshTransform.Apply( mesh, Xform.Translate( -pivot ) );\n\t\t\t\t\t\tWriteModel( studio, mesh, dir, folder, partName );\n\t\t\t\t\t\tlayout.AppendLine( $\u0022{body.Name ?? body.Id}\\t{folder}/{partName}.vmdl\\t{N( pivot.x )}\\t{N( pivot.y )}\\t{N( pivot.z )}\\t{N( max.x - min.x )}\\t{N( max.y - min.y )}\\t{N( max.z - min.z )}\u0022 );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\tvar mesh = new PolyMesh();\n\t\t\t\t\tforeach ( var body in bodies )\n\t\t\t\t\t\tMeshTransform.Append( mesh, body.Mesh );\n\n\t\t\t\t\tvar (min, max) = ModelMeasure.Bounds( mesh );\n\t\t\t\t\tvar pivot = r.Pivots.TryGetValue( name, out var p ) ? p : new Vec3( (min.x \u002B max.x) * 0.5f, (min.y \u002B max.y) * 0.5f, min.z );\n\t\t\t\t\tMeshTransform.Apply( mesh, Xform.Translate( -pivot ) );\n\t\t\t\t\tWriteModel( studio, mesh, dir, folder, Safe( name ) );\n\t\t\t\t\tlayout.AppendLine( $\u0022{name}\\t{folder}/{Safe( name )}.vmdl\\t{N( pivot.x )}\\t{N( pivot.y )}\\t{N( pivot.z )}\\t{N( max.x - min.x )}\\t{N( max.y - min.y )}\\t{N( max.z - min.z )}\u0022 );\n\t\t\t\t}\n\n\t\t\t\tvar layoutPath = Path.Combine( dir, \u0022layout.txt\u0022 );\n\t\t\t\tFile.WriteAllText( layoutPath, layout.ToString() );\n\t\t\t\tr.Written.Add( layoutPath );\n\t\t\t\tr.Log.Add( $\u0022exported {(split ? bodies.Count : 1)} model(s) to {dir}; layout in layout.txt\u0022 );\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tdefault:\n\t\t\t\treturn Fail( r, $\u0022Unknown command \u0027{verb}\u0027. Commands: add, set, remove, rebuild, fix, render, describe, measure, distance, symmetry, match, material, pivot, export, save, echo.\u0022 );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EOne .obj and the .vmdl that loads it, materials bound from the studio\u0027s slots.\u003C/summary\u003E\n\tstatic void WriteModel( PartStudio studio, PolyMesh mesh, string dir, string folder, string name )\n\t{\n\t\tvar objPath = Path.Combine( dir, name \u002B \u0022.obj\u0022 );\n\t\tObjWriter.WriteFile( mesh, objPath, name, materialName: studio.NameForSlot );\n\n\t\tvar materials = VmdlMaterials.GroupList( studio, mesh );\n\t\tFile.WriteAllText( Path.Combine( dir, name \u002B \u0022.vmdl\u0022 ), VmdlDocument.Static( $\u0022{folder}/{name}.obj\u0022, \u0022\u0022, materials ) );\n\t}\n\n\tstatic string Safe( string name )\n\t{\n\t\tvar sb = new StringBuilder();\n\t\tforeach ( var c in (name ?? \u0022part\u0022).Trim().ToLowerInvariant() )\n\t\t\tsb.Append( char.IsLetterOrDigit( c ) ? c : \u0027_\u0027 );\n\t\treturn sb.Length == 0 ? \u0022part\u0022 : sb.ToString();\n\t}\n\n\tstatic string N( float v ) =\u003E v.ToString( \u00220.###\u0022, CultureInfo.InvariantCulture );\n\n\tstatic bool RebuildAndCheck( PartStudio studio, Feature feature, Result r, string did )\n\t{\n\t\tstudio.Rebuild();\n\n\t\tif ( feature.Error is not null )\n\t\t{\n\t\t\tvar d = feature.Diagnostic;\n\t\t\tvar sb = new StringBuilder();\n\t\t\tsb.Append( $\u0022{feature.TypeName} \u0027{feature.Name ?? feature.Id}\u0027 failed: {feature.Error}\u0022 );\n\n\t\t\tif ( d is not null )\n\t\t\t{\n\t\t\t\tif ( !string.IsNullOrEmpty( d.Cause ) ) sb.Append( $\u0022 Because: {d.Cause}\u0022 );\n\t\t\t\tforeach ( var remedy in d.Remedies ) sb.Append( $\u0022 Try: {remedy}.\u0022 );\n\t\t\t}\n\n\t\t\treturn Fail( r, sb.ToString() );\n\t\t}\n\n\t\tr.Log.Add( did \u002B (feature.Warning is not null ? $\u0022 (note: {feature.Warning})\u0022 : \u0022\u0022 ) );\n\t\treturn true;\n\t}\n\n\tstatic bool Fail( Result r, string message )\n\t{\n\t\tr.Error = message;\n\t\treturn false;\n\t}\n\n\t// --- assigning parameters ---------------------------------------------------------------------\n\n\tstatic bool Assign( PartStudio studio, Feature feature, Dictionary\u003Cstring, string\u003E pairs, Result r )\n\t{\n\t\tforeach ( var (key, value) in pairs )\n\t\t{\n\t\t\tif ( key == \u0022name\u0022 )\n\t\t\t{\n\t\t\t\tfeature.Name = value;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tvar target = FindParam( feature, key, out var field );\n\n\t\t\tif ( target is null \u0026\u0026 field is null )\n\t\t\t\treturn Fail( r, $\u0022{feature.TypeName} has no parameter \u0027{key}\u0027. It has: {string.Join( \u0022, \u0022, ParamNames( feature ) )}.\u0022 );\n\n\t\t\ttry\n\t\t\t{\n\t\t\t\tif ( target is not null \u0026\u0026 !AssignParam( studio, target, value, out var why ) )\n\t\t\t\t\treturn Fail( r, $\u0022{key}: {why}\u0022 );\n\n\t\t\t\tif ( target is null \u0026\u0026 !AssignField( studio, feature, field, value, out var whyField ) )\n\t\t\t\t\treturn Fail( r, $\u0022{key}: {whyField}\u0022 );\n\t\t\t}\n\t\t\tcatch ( Exception e )\n\t\t\t{\n\t\t\t\treturn Fail( r, $\u0022{key}: {e.Message}\u0022 );\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\tstatic bool AssignParam( PartStudio studio, IParam param, string value, out string why )\n\t{\n\t\twhy = null;\n\n\t\tswitch ( param )\n\t\t{\n\t\t\tcase FloatParam f:\n\t\t\t\tif ( !TryNumber( value, out var fv ) ) { why = $\u0022\u0027{value}\u0027 is not a number.\u0022; return false; }\n\t\t\t\tf.Value = fv;\n\t\t\t\tf.Expr = null;\n\t\t\t\treturn true;\n\n\t\t\tcase IntParam i:\n\t\t\t\tif ( !TryNumber( value, out var iv ) ) { why = $\u0022\u0027{value}\u0027 is not a number.\u0022; return false; }\n\t\t\t\ti.Value = (int)MathF.Round( iv );\n\t\t\t\treturn true;\n\n\t\t\tcase BoolParam b:\n\t\t\t\tb.Value = Truthy( value );\n\t\t\t\treturn true;\n\n\t\t\tcase ChoiceParam c:\n\t\t\t{\n\t\t\t\tvar index = Array.FindIndex( c.Options, o =\u003E string.Equals( o, value, StringComparison.OrdinalIgnoreCase )\n\t\t\t\t\t|| string.Equals( o.Replace( \u0027 \u0027, \u0027_\u0027 ), value, StringComparison.OrdinalIgnoreCase ) );\n\n\t\t\t\t// \u0022Left\u0022 for \u0022Left (y)\u0022, \u0022box\u0022 for \u0022Box\u0022: the start of an option, then anywhere in it.\n\t\t\t\tif ( index \u003C 0 )\n\t\t\t\t\tindex = Array.FindIndex( c.Options, o =\u003E o.StartsWith( value, StringComparison.OrdinalIgnoreCase ) );\n\n\t\t\t\tif ( index \u003C 0 )\n\t\t\t\t\tindex = Array.FindIndex( c.Options, o =\u003E o.Contains( value, StringComparison.OrdinalIgnoreCase ) );\n\n\t\t\t\tif ( index \u003C 0 \u0026\u0026 int.TryParse( value, out var n ) \u0026\u0026 n \u003E= 0 \u0026\u0026 n \u003C c.Options.Length )\n\t\t\t\t\tindex = n;\n\n\t\t\t\tif ( index \u003C 0 ) { why = $\u0022\u0027{value}\u0027 is not one of {string.Join( \u0022, \u0022, c.Options )}.\u0022; return false; }\n\t\t\t\tc.Index = index;\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tcase Vec3Param v:\n\t\t\t\tif ( !Point( studio, value, out var p, out why ) ) return false;\n\t\t\t\tv.Value = p;\n\t\t\t\treturn true;\n\n\t\t\tcase StringParam s:\n\t\t\t\ts.Value = value;\n\t\t\t\treturn true;\n\n\t\t\tcase BodySelectionParam bodies:\n\t\t\t{\n\t\t\t\tbodies.BodyIds.Clear();\n\n\t\t\t\tforeach ( var name in value.Split( new[] { \u0027,\u0027, \u0027;\u0027 }, StringSplitOptions.RemoveEmptyEntries ) )\n\t\t\t\t{\n\t\t\t\t\tvar body = ModelMeasure.FindBody( studio, name.Trim() );\n\t\t\t\t\tif ( body is null ) { why = $\u0022No body called \u0027{name.Trim()}\u0027. Bodies: {string.Join( \u0022, \u0022, studio.Bodies.Select( b =\u003E b.Name ?? b.Id ) )}.\u0022; return false; }\n\t\t\t\t\tbodies.BodyIds.Add( body.Id );\n\t\t\t\t}\n\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\twhy = $\u0022a {param.GetType().Name} cannot be set from a script.\u0022;\n\t\treturn false;\n\t}\n\n\tstatic bool AssignField( PartStudio studio, Feature feature, FieldInfo field, string value, out string why )\n\t{\n\t\twhy = null;\n\t\tvar current = field.GetValue( feature );\n\n\t\tswitch ( current )\n\t\t{\n\t\t\tcase List\u003CVec3\u003E points:\n\t\t\t{\n\t\t\t\tpoints.Clear();\n\n\t\t\t\tforeach ( var part in value.Split( \u0027;\u0027, StringSplitOptions.RemoveEmptyEntries ) )\n\t\t\t\t{\n\t\t\t\t\tif ( !Point( studio, part.Trim(), out var p, out why ) ) return false;\n\t\t\t\t\tpoints.Add( p );\n\t\t\t\t}\n\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tcase List\u003CVec2\u003E pairs:\n\t\t\t{\n\t\t\t\tpairs.Clear();\n\n\t\t\t\tforeach ( var part in value.Split( \u0027;\u0027, StringSplitOptions.RemoveEmptyEntries ) )\n\t\t\t\t{\n\t\t\t\t\tvar xy = part.Split( \u0027,\u0027 );\n\t\t\t\t\tif ( xy.Length != 2 || !TryNumber( xy[0], out var x ) || !TryNumber( xy[1], out var y ) ) { why = $\u0022\u0027{part}\u0027 is not a pair a,b.\u0022; return false; }\n\t\t\t\t\tpairs.Add( new Vec2( x, y ) );\n\t\t\t\t}\n\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tcase string:\n\t\t\t{\n\t\t\t\t// A field holding another feature\u0027s id \u2014 a path sketch, a spline \u2014 takes its name.\n\t\t\t\tvar other = FindFeature( studio, value );\n\t\t\t\tfield.SetValue( feature, other?.Id ?? value );\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tcase List\u003Cstring\u003E texts:\n\t\t\t{\n\t\t\t\ttexts.Clear();\n\t\t\t\tforeach ( var part in value.Split( new[] { \u0027,\u0027, \u0027;\u0027 }, StringSplitOptions.RemoveEmptyEntries ) )\n\t\t\t\t\ttexts.Add( FindFeature( studio, part.Trim() )?.Id ?? part.Trim() );\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\twhy = $\u0022a {field.FieldType.Name} cannot be set from a script.\u0022;\n\t\treturn false;\n\t}\n\n\tstatic IParam FindParam( Feature feature, string key, out FieldInfo field )\n\t{\n\t\tfield = null;\n\t\tvar wanted = Normalise( key );\n\n\t\tforeach ( var param in feature.Parameters.Concat( feature.AdvancedParameters ) )\n\t\t\tif ( Normalise( param.Label ) == wanted )\n\t\t\t\treturn param;\n\n\t\tforeach ( var f in feature.GetType().GetFields( BindingFlags.Public | BindingFlags.Instance ) )\n\t\t{\n\t\t\tif ( Normalise( f.Name ) != wanted )\n\t\t\t\tcontinue;\n\n\t\t\tif ( f.GetValue( feature ) is IParam p )\n\t\t\t\treturn p;\n\n\t\t\tfield = f;\n\t\t\treturn null;\n\t\t}\n\n\t\treturn null;\n\t}\n\n\tstatic IEnumerable\u003Cstring\u003E ParamNames( Feature feature )\n\t{\n\t\tforeach ( var p in feature.Parameters.Concat( feature.AdvancedParameters ).Distinct() )\n\t\t\tyield return p.Label;\n\n\t\tforeach ( var f in feature.GetType().GetFields( BindingFlags.Public | BindingFlags.Instance ) )\n\t\t\tif ( f.GetValue( feature ) is not IParam \u0026\u0026 f.Name is not (\u0022Id\u0022 or \u0022Name\u0022 or \u0022Suppressed\u0022 or \u0022Visible\u0022) )\n\t\t\t\tyield return f.Name;\n\t}\n\n\tstatic string Normalise( string s ) =\u003E (s ?? \u0022\u0022).ToLowerInvariant().Replace( \u0022 \u0022, \u0022\u0022 ).Replace( \u0022_\u0022, \u0022\u0022 ).Replace( \u0022\u0027\u0022, \u0022\u0022 );\n\n\t// --- points and landmarks ---------------------------------------------------------------------\n\n\t/// \u003Csummary\u003E\u0060x,y,z\u0060, \u0060@landmark\u0060, or \u0060@landmark\u002Bdx,dy,dz\u0060 / \u0060@landmark-dx,dy,dz\u0060.\u003C/summary\u003E\n\tpublic static bool Point( PartStudio studio, string text, out Vec3 point, out string why )\n\t{\n\t\twhy = null;\n\t\tpoint = Vec3.Zero;\n\t\ttext = (text ?? \u0022\u0022).Trim();\n\n\t\tif ( !text.StartsWith( \u0027@\u0027 ) )\n\t\t{\n\t\t\tif ( ModelMeasure.TryParseVec3( text, out point ) )\n\t\t\t\treturn true;\n\n\t\t\t// A bare name is a landmark too.\n\t\t\treturn ModelMeasure.TryLandmark( studio, text, out point, out why );\n\t\t}\n\n\t\tvar body = text[1..];\n\t\tvar offset = Vec3.Zero;\n\t\tvar plus = body.IndexOfAny( new[] { \u0027\u002B\u0027, \u0027-\u0027 }, 1 );\n\n\t\tif ( plus \u003E 0 )\n\t\t{\n\t\t\tvar sign = body[plus] == \u0027-\u0027 ? -1f : 1f;\n\t\t\tvar offsetText = body[(plus \u002B 1)..];\n\t\t\tbody = body[..plus];\n\n\t\t\tif ( !ModelMeasure.TryParseVec3( offsetText, out offset ) )\n\t\t\t{\n\t\t\t\twhy = $\u0022\u0027{offsetText}\u0027 after the landmark is not an offset dx,dy,dz.\u0022;\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\toffset *= sign;\n\t\t}\n\n\t\tif ( !ModelMeasure.TryLandmark( studio, body, out point, out why ) )\n\t\t\treturn false;\n\n\t\tpoint \u002B= offset;\n\t\treturn true;\n\t}\n\n\t// --- features by type and name ----------------------------------------------------------------\n\n\tstatic Feature Create( string typeName )\n\t{\n\t\tvar wanted = Normalise( typeName );\n\n\t\tforeach ( var type in FeatureTypes() )\n\t\t{\n\t\t\tvar name = Normalise( type.Name );\n\t\t\tvar bare = name.EndsWith( \u0022feature\u0022 ) ? name[..^7] : name;\n\n\t\t\tif ( name == wanted || bare == wanted )\n\t\t\t\treturn (Feature)Activator.CreateInstance( type );\n\t\t}\n\n\t\treturn null;\n\t}\n\n\tstatic IEnumerable\u003CType\u003E FeatureTypes() =\u003E typeof( Feature ).Assembly.GetTypes()\n\t\t.Where( t =\u003E t.IsSubclassOf( typeof( Feature ) ) \u0026\u0026 !t.IsAbstract )\n\t\t.OrderBy( t =\u003E t.Name, StringComparer.Ordinal );\n\n\t/// \u003Csummary\u003EA feature type\u0027s parameters, one per line, with what each takes \u2014 the help a script\n\t/// author reads before writing \u0060add\u0060.\u003C/summary\u003E\n\tpublic static string Describe( string typeName )\n\t{\n\t\tvar feature = Create( typeName );\n\n\t\tif ( feature is null )\n\t\t\treturn $\u0022No feature type \u0027{typeName}\u0027. Types: {string.Join( \u0022, \u0022, FeatureTypeNames() )}.\u0022;\n\n\t\tvar sb = new StringBuilder();\n\t\tsb.AppendLine( $\u0022{feature.TypeName} (add {typeName})\u0022 );\n\n\t\tforeach ( var param in feature.Parameters.Concat( feature.AdvancedParameters ).Distinct() )\n\t\t{\n\t\t\tvar key = Normalise( param.Label );\n\t\t\tvar kind = param switch\n\t\t\t{\n\t\t\t\tFloatParam f =\u003E $\u0022number, default {f.Value}\u0022 \u002B (f.Min \u003E float.MinValue ? $\u0022, min {f.Min}\u0022 : \u0022\u0022) \u002B (f.Max \u003C float.MaxValue ? $\u0022, max {f.Max}\u0022 : \u0022\u0022),\n\t\t\t\tIntParam i =\u003E $\u0022whole number, default {i.Value}, {i.Min}..{i.Max}\u0022,\n\t\t\t\tBoolParam b =\u003E $\u0022on/off, default {(b.Value ? \u0022on\u0022 : \u0022off\u0022)}\u0022,\n\t\t\t\tChoiceParam c =\u003E $\u0022one of {string.Join( \u0022|\u0022, c.Options )}, default {c.Value}\u0022,\n\t\t\t\tVec3Param v =\u003E $\u0022point x,y,z or @landmark, default {v.Value.x},{v.Value.y},{v.Value.z}\u0022,\n\t\t\t\tStringParam =\u003E \u0022text\u0022,\n\t\t\t\tBodySelectionParam =\u003E \u0022body names, comma-separated (empty = every body)\u0022,\n\t\t\t\t_ =\u003E param.GetType().Name,\n\t\t\t};\n\t\t\tsb.AppendLine( $\u0022  {key} = {kind}\u0022 );\n\t\t}\n\n\t\tforeach ( var f in feature.GetType().GetFields( BindingFlags.Public | BindingFlags.Instance ) )\n\t\t{\n\t\t\tif ( f.GetValue( feature ) is IParam || f.Name is \u0022Id\u0022 or \u0022Name\u0022 or \u0022Suppressed\u0022 or \u0022Visible\u0022 )\n\t\t\t\tcontinue;\n\n\t\t\tvar kind = f.FieldType == typeof( List\u003CVec3\u003E ) ? \u0022points x,y,z;x,y,z;\u2026 (landmarks allowed)\u0022\n\t\t\t\t: f.FieldType == typeof( List\u003CVec2\u003E ) ? \u0022pairs a,b;a,b;\u2026\u0022\n\t\t\t\t: f.FieldType == typeof( string ) ? \u0022a feature name\u0022\n\t\t\t\t: f.FieldType == typeof( List\u003Cstring\u003E ) ? \u0022feature names, comma-separated\u0022\n\t\t\t\t: null;\n\n\t\t\tif ( kind is not null )\n\t\t\t\tsb.AppendLine( $\u0022  {Normalise( f.Name )} = {kind}\u0022 );\n\t\t}\n\n\t\tsb.AppendLine( \u0022  name = what to call it\u0022 );\n\t\treturn sb.ToString().TrimEnd();\n\t}\n\n\tpublic static IEnumerable\u003Cstring\u003E FeatureTypeNames() =\u003E\n\t\tFeatureTypes().Select( t =\u003E t.Name.EndsWith( \u0022Feature\u0022 ) ? t.Name[..^7] : t.Name );\n\n\tstatic Feature FindFeature( PartStudio studio, string name )\n\t{\n\t\tif ( string.IsNullOrWhiteSpace( name ) )\n\t\t\treturn null;\n\n\t\tif ( name.StartsWith( \u0027#\u0027 ) \u0026\u0026 int.TryParse( name[1..], out var index ) \u0026\u0026 index \u003E= 1 \u0026\u0026 index \u003C= studio.Features.Count )\n\t\t\treturn studio.Features[index - 1];\n\n\t\treturn studio.Features.FirstOrDefault( f =\u003E f.Id == name )\n\t\t\t?? studio.Features.LastOrDefault( f =\u003E string.Equals( f.Name, name, StringComparison.OrdinalIgnoreCase ) )\n\t\t\t?? studio.Features.LastOrDefault( f =\u003E string.Equals( f.Name?.Replace( \u0027 \u0027, \u0027_\u0027 ), name, StringComparison.OrdinalIgnoreCase ) )\n\t\t\t?? studio.Features.LastOrDefault( f =\u003E string.Equals( f.TypeName, name, StringComparison.OrdinalIgnoreCase ) );\n\t}\n\n\t// --- lexing -------------------------------------------------------------------------------------\n\n\tstatic List\u003Cstring\u003E Tokenise( string line )\n\t{\n\t\tvar tokens = new List\u003Cstring\u003E();\n\t\tvar sb = new StringBuilder();\n\t\tvar quoted = false;\n\n\t\tforeach ( var c in line )\n\t\t{\n\t\t\tif ( c == \u0027\u0022\u0027 ) { quoted = !quoted; continue; }\n\n\t\t\tif ( char.IsWhiteSpace( c ) \u0026\u0026 !quoted )\n\t\t\t{\n\t\t\t\tif ( sb.Length \u003E 0 ) { tokens.Add( sb.ToString() ); sb.Clear(); }\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tsb.Append( c );\n\t\t}\n\n\t\tif ( sb.Length \u003E 0 )\n\t\t\ttokens.Add( sb.ToString() );\n\n\t\treturn tokens;\n\t}\n\n\tstatic Dictionary\u003Cstring, string\u003E Pairs( IEnumerable\u003Cstring\u003E tokens )\n\t{\n\t\tvar pairs = new Dictionary\u003Cstring, string\u003E( StringComparer.OrdinalIgnoreCase );\n\n\t\tforeach ( var token in tokens )\n\t\t{\n\t\t\tvar eq = token.IndexOf( \u0027=\u0027 );\n\n\t\t\tif ( eq \u003C= 0 )\n\t\t\t\tthrow new ArgumentException( $\u0022\u0027{token}\u0027 is not key=value.\u0022 );\n\n\t\t\tpairs[token[..eq].Trim().ToLowerInvariant()] = token[(eq \u002B 1)..].Trim();\n\t\t}\n\n\t\treturn pairs;\n\t}\n\n\tstatic bool TryNumber( string text, out float value ) =\u003E\n\t\tfloat.TryParse( (text ?? \u0022\u0022).Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out value );\n\n\tstatic float Number( string text ) =\u003E TryNumber( text, out var v ) ? v : throw new ArgumentException( $\u0022\u0027{text}\u0027 is not a number.\u0022 );\n\n\tstatic bool Truthy( string text ) =\u003E (text ?? \u0022\u0022).Trim().ToLowerInvariant() is \u00221\u0022 or \u0022true\u0022 or \u0022on\u0022 or \u0022yes\u0022;\n\n\tstatic string Resolve( string baseDir, string path ) =\u003E Path.IsPathRooted( path ) ? path : Path.Combine( baseDir, path );\n}\n"},{"Ident":"pooh.geppetto","Path":"Editor/Effigy/Sketch/SketchReference.cs","FileName":"SketchReference.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":411088,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\n\nnamespace Effigy;\n\n/// \u003Csummary\u003E\n/// Geometry the sketch does not own, projected into its plane so it can be seen and snapped to.\n///\n/// WHAT THIS IS FOR. A sketch on the face of an existing part is nearly always ABOUT that face \u2014\n/// a boss centred on it, a pocket set in from one of its corners, a rib running along one of its\n/// edges. Until this existed the face went blank the moment the sketcher opened: the plane was\n/// derived from it and then nothing about it was drawn or snappable, so lining a new rectangle up\n/// with the edge directly underneath it was done by eye against the shaded solid. That is exactly\n/// the kind of \u0022close enough\u0022 that turns into a 0.03-unit sliver after an extrude.\n///\n/// EVERY OTHER CAD PACKAGE CALLS THIS PROJECTED REFERENCE GEOMETRY and makes you ask for it a\n/// curve at a time \u2014 Onshape\u0027s Use tool, SolidWorks\u0027 Convert Entities. Effigy shows the whole\n/// face\u0027s boundary automatically instead, because the face was CHOSEN as the plane a moment ago,\n/// which is a much stronger statement of intent than clicking one edge of it.\n///\n/// IT IS NOT PART OF THE SKETCH. Nothing here is in Sketch.Points or Sketch.Curves, so it never\n/// reaches ProfileFinder, never extrudes, and is never saved. It is rebuilt from the model every\n/// time the sketch is opened, which is what keeps it honest when the face underneath changes \u2014\n/// the same reason FaceRef stores geometry rather than \u0022Face6\u0022.\n/// \u003C/summary\u003E\npublic sealed class SketchReference\n{\n\t/// \u003Csummary\u003ECorners of the referenced geometry, in sketch-plane coordinates.\u003C/summary\u003E\n\tpublic readonly List\u003CVec2\u003E Points = new();\n\n\t/// \u003Csummary\u003EEdges, as index pairs into \u003Csee cref=\u0022Points\u0022/\u003E.\u003C/summary\u003E\n\tpublic readonly List\u003C(int A, int B)\u003E Edges = new();\n\n\tpublic bool IsEmpty =\u003E Points.Count == 0;\n\n\t/// \u003Csummary\u003EAn edge as the two positions it runs between. Bounds-checked to a zero-length\n\t/// segment rather than throwing: this is drawn and hit-tested every frame from an index that a\n\t/// rebuild underneath could have invalidated, and a viewport that throws once per frame is\n\t/// worse than one that briefly draws nothing.\u003C/summary\u003E\n\tpublic (Vec2 A, Vec2 B) Segment( int index )\n\t{\n\t\tif ( index \u003C 0 || index \u003E= Edges.Count )\n\t\t\treturn (Vec2.Zero, Vec2.Zero);\n\n\t\tvar (a, b) = Edges[index];\n\n\t\tif ( a \u003C 0 || a \u003E= Points.Count || b \u003C 0 || b \u003E= Points.Count )\n\t\t\treturn (Vec2.Zero, Vec2.Zero);\n\n\t\treturn (Points[a], Points[b]);\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Copy one reference edge into the sketch as a real line \u2014 Onshape\u0027s Use, one edge at a time.\n\t///\n\t/// WHY A COPY AND NOT A LIVE LINK. Onshape\u0027s projected curves stay attached to what they were\n\t/// taken from and move when it moves. That is the better behaviour and it is also a whole\n\t/// feature: the sketch would need a second class of curve that the user cannot drag or delete,\n\t/// that ProfileFinder unions in, and that is rebuilt rather than saved. A copy is what the tool\n\t/// does here, and it is honest about it \u2014 the line becomes ordinary sketch geometry, yours to\n\t/// trim and drag, and it does NOT follow the face afterwards.\n\t///\n\t/// Points are reused through SketchSnapper.PointIndex, so an edge copied in welds onto whatever\n\t/// is already at its ends rather than laying a second point on top of the first. That is what\n\t/// makes \u0022use all four edges, then draw a line across\u0022 close into two regions instead of into\n\t/// nothing at all.\n\t/// \u003C/summary\u003E\n\t/// \u003Creturns\u003EThe line added, or null when the edge is degenerate or the sketch already has it.\u003C/returns\u003E\n\tpublic SketchLine UseEdge( Sketch sketch, int edgeIndex )\n\t{\n\t\tif ( sketch is null || edgeIndex \u003C 0 || edgeIndex \u003E= Edges.Count )\n\t\t\treturn null;\n\n\t\tvar (a, b) = Segment( edgeIndex );\n\n\t\tvar start = SketchSnapper.PointIndex( sketch, a );\n\t\tvar end = SketchSnapper.PointIndex( sketch, b );\n\n\t\t// A zero-length line is not geometry - ProfileFinder links it into the adjacency map twice\n\t\t// at one point and calls the sketch branching. The line tool has the same guard.\n\t\tif ( start == end )\n\t\t\treturn null;\n\n\t\t// USING THE SAME EDGE TWICE MUST NOT LAY A SECOND LINE ON THE FIRST. Two curves between the\n\t\t// same pair of points is exactly the branching case ProfileFinder refuses, so clicking an\n\t\t// edge you already used would quietly destroy the profile you were building.\n\t\tforeach ( var curve in sketch.Curves )\n\t\t{\n\t\t\tvar (from, to) = curve.Endpoints;\n\n\t\t\tif ( (from == start \u0026\u0026 to == end) || (from == end \u0026\u0026 to == start) )\n\t\t\t\treturn null;\n\t\t}\n\n\t\treturn sketch.Add( new SketchLine( start, end ) );\n\t}\n\n\t/// \u003Csummary\u003ECopy every reference edge in, which is the common case: the whole face outline, so a\n\t/// single line drawn across it closes two regions. Returns how many were added \u2014 edges already\n\t/// in the sketch are skipped, so running it twice is harmless.\u003C/summary\u003E\n\tpublic int UseAll( Sketch sketch )\n\t{\n\t\tvar added = 0;\n\n\t\tfor ( var i = 0; i \u003C Edges.Count; i\u002B\u002B )\n\t\t{\n\t\t\tif ( UseEdge( sketch, i ) is not null )\n\t\t\t\tadded\u002B\u002B;\n\t\t}\n\n\t\treturn added;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The boundary of the face a sketch is attached to, in that sketch\u0027s plane.\n\t///\n\t/// THE BOUNDARY OF THE SURFACE, NOT OF ONE n-GON. A face that has been through a boolean is\n\t/// usually several faces sharing a plane, and outlining each of them separately draws the seams\n\t/// where they were split \u2014 lines that are not edges of anything, sitting in the middle of what\n\t/// looks like one flat surface, and snapping to them is snapping to an artefact of how the mesh\n\t/// happens to be cut up. FaceSurface is what decides where the surface stops, and it is the\n\t/// same answer the viewport highlights and the edge picker offers: three things that used to\n\t/// each work it out for themselves and disagree on screen at the same time.\n\t///\n\t/// PROJECTED, NOT INTERSECTED, so a sketch with an offset still gets the face\u0027s outline \u2014\n\t/// directly below where it will be drawn, which is what makes the offset useful for a boss\n\t/// standing clear of the surface it grows from.\n\t/// \u003C/summary\u003E\n\tpublic static SketchReference FromFace( IEnumerable\u003CBody\u003E bodies, FaceRef reference, SketchPlane plane )\n\t{\n\t\tvar result = new SketchReference();\n\n\t\tif ( plane is null || !FacePlane.TryResolveFace( bodies, reference, out var body, out var faceIndex ) )\n\t\t\treturn result;\n\n\t\tvar mesh = body.Mesh;\n\n\t\t// Scaled to the part, for the same reason every other tolerance in the sketcher is: a\n\t\t// constant that is generous on a 100-unit block silently merges every vertex of a 0.1-unit\n\t\t// one. See SketchSnapper\u0027s header for what fixed tolerances did to this sketcher.\n\t\tvar tolerance = MathF.Max( mesh.BoundsDiagonal * 1e-4f, 1e-5f );\n\n\t\tvar surface = FaceSurface.FromFace( mesh, faceIndex );\n\t\tvar mapped = new Dictionary\u003Cint, int\u003E();\n\n\t\tforeach ( var (from, to) in surface.Boundary )\n\t\t{\n\t\t\tvar a = Map( result, mapped, mesh, plane, tolerance, from );\n\t\t\tvar b = Map( result, mapped, mesh, plane, tolerance, to );\n\n\t\t\tif ( a != b )\n\t\t\t\tresult.Edges.Add( (a, b) );\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/// \u003Csummary\u003EMesh vertex to reference point, projected and de-duplicated. Two mesh vertices at\n\t/// the same position \u2014 which a boolean leaves behind routinely \u2014 must become ONE snap target,\n\t/// or the corner of the face has two dots on it and the cursor picks between them at\n\t/// random.\u003C/summary\u003E\n\tstatic int Map( SketchReference result, Dictionary\u003Cint, int\u003E mapped, PolyMesh mesh,\n\t\tSketchPlane plane, float tolerance, int vertex )\n\t{\n\t\tif ( mapped.TryGetValue( vertex, out var existing ) )\n\t\t\treturn existing;\n\n\t\tvar p = plane.ToPlane( mesh.Positions[vertex] );\n\n\t\tfor ( var i = 0; i \u003C result.Points.Count; i\u002B\u002B )\n\t\t{\n\t\t\tif ( (result.Points[i] - p).LengthSquared \u003E tolerance * tolerance )\n\t\t\t\tcontinue;\n\n\t\t\tmapped[vertex] = i;\n\t\t\treturn i;\n\t\t}\n\n\t\tresult.Points.Add( p );\n\t\tmapped[vertex] = result.Points.Count - 1;\n\n\t\treturn result.Points.Count - 1;\n\t}\n}\n"},{"Ident":"pooh.geppetto","Path":"Editor/Effigy/SkinBlockout.cs","FileName":"SkinBlockout.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":411088,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\n\nnamespace Effigy;\n\n/// \u003Csummary\u003E\n/// A body from a skeleton \u2014 Blender\u0027s Skin modifier, aimed at a rig instead of a wire. Every\n/// joint becomes a cube the size of its radius, every bone a tube between the two cubes it joins,\n/// and a couple of levels of Catmull-Clark turn the result into a smooth, all-quad blob that is\n/// already a closed solid with a clean flow along each limb. The block-out a character starts\n/// from, in one click, on the bones it will be rigged to.\n///\n/// SIX BONES PER JOINT AT MOST: each takes one face of the joint\u0027s cube. A joint with more is\n/// how a real skeleton never looks, so the seventh bone is refused rather than fudged.\n/// \u003C/summary\u003E\npublic static class SkinBlockout\n{\n\t/// \u003Csummary\u003E\n\t/// Build the body. \u003Cparamref name=\u0022radiusOf\u0022/\u003E gives each bone\u0027s thickness, or null for a\n\t/// fifth of its length; \u003Cparamref name=\u0022smoothLevels\u0022/\u003E is how many times to subdivide the\n\t/// blocky skin (0 keeps the cubes and tubes, which is what to edit by hand).\n\t/// \u003C/summary\u003E\n\tpublic static PolyMesh Build( Skeleton skeleton, Func\u003Cint, float\u003E radiusOf = null, int smoothLevels = 2 )\n\t{\n\t\tif ( skeleton is null || skeleton.Count == 0 )\n\t\t\tthrow new InvalidOperationException( \u0022There are no bones to build a body on. Draw a rig first.\u0022 );\n\n\t\tradiusOf ??= i =\u003E MathF.Max( skeleton.Bones[i].Length * 0.2f, 0.01f );\n\n\t\t// Joints: heads and tails, merged where a child\u0027s head sits on its parent\u0027s tail.\n\t\tvar nodes = new List\u003CVec3\u003E();\n\t\tvar nodeRadius = new List\u003Cfloat\u003E();\n\t\tint NodeAt( Vec3 p, float r )\n\t\t{\n\t\t\tfor ( var i = 0; i \u003C nodes.Count; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tif ( (nodes[i] - p).LengthSquared \u003C 1e-4f )\n\t\t\t\t{\n\t\t\t\t\tnodeRadius[i] = MathF.Max( nodeRadius[i], r );\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tnodes.Add( p );\n\t\t\tnodeRadius.Add( r );\n\t\t\treturn nodes.Count - 1;\n\t\t}\n\n\t\tvar bones = new List\u003C(int head, int tail)\u003E();\n\t\tfor ( var b = 0; b \u003C skeleton.Count; b\u002B\u002B )\n\t\t{\n\t\t\tvar head = skeleton.HeadWorld( b );\n\t\t\tvar tail = skeleton.TailWorld( b );\n\t\t\tif ( (tail - head).LengthSquared \u003C 1e-6f )\n\t\t\t\tcontinue;\n\n\t\t\tvar r = MathF.Max( radiusOf( b ), 0.01f );\n\t\t\tbones.Add( (NodeAt( head, r ), NodeAt( tail, r )) );\n\t\t}\n\n\t\tif ( bones.Count == 0 )\n\t\t\tthrow new InvalidOperationException( \u0022Every bone has no length, so there is nothing to build along.\u0022 );\n\n\t\t// A cube per joint: eight corners, six faces, each face free until a bone claims it.\n\t\tvar mesh = new PolyMesh();\n\t\tvar cubeCorner = new int[nodes.Count][];\n\t\tvar faceTaken = new bool[nodes.Count][];\n\t\tfor ( var n = 0; n \u003C nodes.Count; n\u002B\u002B )\n\t\t{\n\t\t\tvar c = nodes[n];\n\t\t\tvar r = nodeRadius[n];\n\t\t\tcubeCorner[n] = new int[8];\n\t\t\tfor ( var k = 0; k \u003C 8; k\u002B\u002B )\n\t\t\t{\n\t\t\t\tcubeCorner[n][k] = mesh.Positions.Count;\n\t\t\t\tmesh.Positions.Add( c \u002B new Vec3( (k \u0026 1) == 0 ? -r : r, (k \u0026 2) == 0 ? -r : r, (k \u0026 4) == 0 ? -r : r ) );\n\t\t\t}\n\n\t\t\tfaceTaken[n] = new bool[6];\n\t\t}\n\n\t\t// The six faces of a cube, wound outward, and the way each looks.\n\t\tvar faceNormals = new[] { new Vec3( 1, 0, 0 ), new Vec3( -1, 0, 0 ), new Vec3( 0, 1, 0 ), new Vec3( 0, -1, 0 ), new Vec3( 0, 0, 1 ), new Vec3( 0, 0, -1 ) };\n\t\tvar faceCorners = new[]\n\t\t{\n\t\t\tnew[] { 1, 3, 7, 5 },   // \u002Bx\n\t\t\tnew[] { 0, 4, 6, 2 },   // -x\n\t\t\tnew[] { 2, 6, 7, 3 },   // \u002By\n\t\t\tnew[] { 0, 1, 5, 4 },   // -y\n\t\t\tnew[] { 4, 5, 7, 6 },   // \u002Bz\n\t\t\tnew[] { 0, 2, 3, 1 },   // -z\n\t\t};\n\n\t\tint Claim( int node, Vec3 direction )\n\t\t{\n\t\t\tvar best = -1;\n\t\t\tvar bestDot = float.MinValue;\n\t\t\tfor ( var f = 0; f \u003C 6; f\u002B\u002B )\n\t\t\t{\n\t\t\t\tif ( faceTaken[node][f] )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tvar d = Vec3.Dot( faceNormals[f], direction );\n\t\t\t\tif ( d \u003E bestDot )\n\t\t\t\t{\n\t\t\t\t\tbestDot = d;\n\t\t\t\t\tbest = f;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( best \u003C 0 )\n\t\t\t\tthrow new InvalidOperationException( \u0022A joint has more than six bones meeting at it, which is more than a body can be built round.\u0022 );\n\n\t\t\tfaceTaken[node][best] = true;\n\t\t\treturn best;\n\t\t}\n\n\t\t// Tubes: each bone claims a face at each end and bridges them.\n\t\tforeach ( var (head, tail) in bones )\n\t\t{\n\t\t\tvar axis = (nodes[tail] - nodes[head]).Normal;\n\t\t\tvar fa = Claim( head, axis );\n\t\t\tvar fb = Claim( tail, axis * -1f );\n\n\t\t\tvar a = new int[4];\n\t\t\tvar b = new int[4];\n\t\t\tfor ( var i = 0; i \u003C 4; i\u002B\u002B )\n\t\t\t{\n\t\t\t\ta[i] = cubeCorner[head][faceCorners[fa][i]];\n\t\t\t\tb[i] = cubeCorner[tail][faceCorners[fb][3 - i]];\n\t\t\t}\n\n\t\t\t// Turn the far ring so its first corner is the one nearest the near ring\u0027s first,\n\t\t\t// measured across the axis, so the tube does not twist.\n\t\t\tvar start = 0;\n\t\t\tvar bestD = float.MaxValue;\n\t\t\tvar pa = mesh.Positions[a[0]] - axis * Vec3.Dot( mesh.Positions[a[0]] - nodes[head], axis );\n\t\t\tfor ( var j = 0; j \u003C 4; j\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar pb = mesh.Positions[b[j]] - axis * Vec3.Dot( mesh.Positions[b[j]] - nodes[head], axis );\n\t\t\t\tvar d = (pb - pa).LengthSquared;\n\t\t\t\tif ( d \u003C bestD )\n\t\t\t\t{\n\t\t\t\t\tbestD = d;\n\t\t\t\t\tstart = j;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor ( var i = 0; i \u003C 4; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar i2 = (i \u002B 1) % 4;\n\t\t\t\tvar j = (start \u002B i) % 4;\n\t\t\t\tvar j2 = (start \u002B i \u002B 1) % 4;\n\t\t\t\tmesh.AddFace( new[] { a[i], a[i2], b[j2], b[j] } );\n\t\t\t}\n\t\t}\n\n\t\t// Whatever faces no bone claimed stay: they cap the ends of limbs and close the joints.\n\t\tfor ( var n = 0; n \u003C nodes.Count; n\u002B\u002B )\n\t\t{\n\t\t\tfor ( var f = 0; f \u003C 6; f\u002B\u002B )\n\t\t\t{\n\t\t\t\tif ( faceTaken[n][f] )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tvar idx = new int[4];\n\t\t\t\tfor ( var i = 0; i \u003C 4; i\u002B\u002B )\n\t\t\t\t\tidx[i] = cubeCorner[n][faceCorners[f][i]];\n\t\t\t\tmesh.AddFace( idx );\n\t\t\t}\n\t\t}\n\n\t\treturn smoothLevels \u003E 0 ? CatmullClark.Subdivide( mesh, smoothLevels ) : mesh;\n\t}\n}\n"},{"Ident":"pooh.geppetto","Path":"Editor/EffigyEditor/EffigyIcons.cs","FileName":"EffigyIcons.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":411088,"IsPrivate":false,"Code":"using Editor;\nusing System.Collections.Generic;\nusing Sandbox;\nusing System;\n\nnamespace Marionette.EditorTools;\n\n/// \u003Csummary\u003EWhich drawing a feature-tool button paints. One per creation tool.\u003C/summary\u003E\ninternal enum EffigyIcon\n{\n\tSketch,\n\tPlane,\n\tPrimitive,\n\tExtrude,\n\tRevolve,\n\tSweep,\n\tLoft,\n\tChamfer,\n\tFillet,\n\tShell,\n\tSubdivide,\n\n\t// Subdivide\u0027s opposite number, and drawn as it: the same square, with the fine grid it is\n\t// losing shown faint underneath the coarse one it keeps.\n\tRemesh,\n\n\tMirror,\n\tLinearPattern,\n\tCircularPattern,\n\tTransform,\n\tUVProject,\n\tFaceMaterial,\n\tBoolean,\n\n\t// --- sketch tools -------------------------------------------------------------------------\n\t// These were Material Icon NAMES until now, and generic ones: show_chart (a zigzag line chart)\n\t// for Line, cached (two refresh arrows) for Arc, crop_square for Rectangle. They said nothing\n\t// about the operation, and half of them said something actively misleading.\n\tSelectTool,\n\tLineTool,\n\tLineMidpointTool,\n\tRectangleTool,\n\tRectangleCentreTool,\n\tCircleTool,\n\tCircleThreePointTool,\n\tArcTool,\n\tArcThreePointTool,\n\tPolygonTool,\n\tPolygonCircumscribedTool,\n\tSlotTool,\n\tPointTool,\n\tConstructionTool,\n\tProfileInspectorTool,\n\tFinishSketchTool,\n\n\t// --- sculpt tools -------------------------------------------------------------------------\n\t// The brushes are drawn as what they DO to a surface rather than as tool shapes: a row of six\n\t// identical brush heads distinguished by a tiny badge is six ways to pick the wrong one. Every\n\t// glyph here is a surface line and what the brush does to it.\n\tSculpt,\n\tSculptDraw,\n\tSculptSmooth,\n\tSculptInflate,\n\tSculptGrab,\n\tSculptFlatten,\n\tSculptPinch,\n\tSculptMask,\n\tSculptLevelDown,\n\tSculptLevelUp,\n\tSculptBake,\n\n\t// --- paint tools ---------------------------------------------------------------------------\n\tPaint,\n\tPaintBrush,\n\tPaintEraser,\n\n\t// --- solid tools that act on picked faces ---------------------------------------------------\n\tDraft,\n\tMoveFace,\n\tHole,\n\n\t// --- the six sketch tools whose kernel half was finished first ------------------------------\n\tEllipseTool,\n\tSplineTool,\n\tTrimTool,\n\tExtendTool,\n\tSketchFilletTool,\n\tOffsetTool,\n\tMirrorTool,\n\n\t// --- taking the face\u0027s own outline into the sketch -------------------------------------------\n\tUseTool,\n\tUseAllTool,\n\n\t// --- the one sketch tool driven by a drag ----------------------------------------------------\n\tCutTool,\n\n\t// --- grease pencil: annotation, not geometry -------------------------------------------------\n\t// Both are drawn as the real-world objects rather than as marks, because that is the one thing\n\t// that says \u0022this is not a modelling operation\u0022 before the tooltip gets a chance to. Every other\n\t// glyph in the bar is a shape being changed; these are stationery.\n\tNoteTool,\n\tNoteEraseTool,\n\n\t// --- lighting: viewport scenery, not geometry ------------------------------------------------\n\t// Drawn as lamps and as what a lamp DOES to a shape, for the same reason the notes above are\n\t// drawn as stationery: nothing on this stage changes the model, and the glyphs should say so\n\t// before the tooltip gets a chance to. The three rigs are the same sphere lit three ways, which\n\t// is the only honest way to draw the difference between them.\n\tLightFullBright,\n\tLightPoint,\n\tLightSpot,\n\tLightSun,\n\tLightRigThreePoint,\n\tLightRigRim,\n\tLightRigTop,\n\tLightRigKey,\n\tLightClear,\n\n\t// --- rig ---\n\tBone,\n\tBoneBind,\n\tBoneSoft,\n\tSoftPreview,\n\tSoftRest,\n\n\t// --- Model \u003E Edit: direct polygon editing ------------------------------------------------------\n\t// Drawn as a small quad cage with the thing being picked or changed lit, so the three pick\n\t// modes read as \u0022which part of the same cage\u0022 and every operation reads as what it does to it.\n\tMeshEdit,\n\tSelectVertex,\n\tSelectEdge,\n\tSelectFace,\n\tInset,\n\tLoopCut,\n\tMerge,\n\tDissolve,\n\tDeleteGeometry,\n\tShrinkwrap,\n\tSolidify,\n\tDrape,\n\tFabric,\n\n\t// --- UV: where the texture is cut and how it is laid out ------------------------------------\n\tSeam,\n\tUnwrap,\n\n\t// --- Clothing: trim hung off a garment\u0027s edge ------------------------------------------------\n\t// A scalloped edge: the wave a gathered strip takes. Drawn as the EDGE rather than as a frill in\n\t// the round, because what the tool does is decorate a boundary, and the boundary is the part a\n\t// person is choosing.\n\tFrill,\n\n\t// --- Clothing: the body being dressed -------------------------------------------------------\n\t// A dressmaker\u0027s dummy, not a person and not a skeleton. The two things it has to say at a\n\t// glance are \u0022this is a whole body\u0022 and \u0022the body is not the thing you are making\u0022 - a stand\n\t// says both, which neither a figure nor a bone does.\n\tWearer,\n}\n\n/// \u003Csummary\u003E\n/// Painted icons for the feature-creation strip, drawn rather than looked up in a font.\n///\n/// Same reasoning as RigIconButton (see Editor/RigControlEditor): s\u0026amp;box ships CLASSIC Material\n/// Icons, not the newer Material Symbols, so a name from the Symbols set silently renders as\n/// nothing \u2014 and the strip was leaning on generic names like \u0022square\u0022, \u0022flip\u0022 and \u0022call_made\u0022\n/// that, where they resolved at all, said nothing about the CAD operation behind them. A drawn\n/// glyph can show the actual operation: Chamfer cuts a corner off a square, Shell puts a wall\n/// inside one, Mirror reflects a solid shape into an outlined one.\n///\n/// Every icon is drawn around \u003Cc\u003Ecenter\u003C/c\u003E inside a nominal 18x18 box, so they all read at the\n/// same weight, then scaled up as one by the \u003Cc\u003Escale\u003C/c\u003E argument to fit the button drawing it.\n/// \u003C/summary\u003E\ninternal static class EffigyIcons\n{\n\t/// \u003Csummary\u003EStroke width every outline uses, so no icon looks heavier than its neighbours.\u003C/summary\u003E\n\tprivate const float Stroke = 1.6f;\n\n\t// --- the pencil\u0027s own colours -------------------------------------------------------------\n\t//\n\t// The ONLY icon that does not draw entirely in the colour it is handed. Sketch is the tool\n\t// every part starts with and the only button in the strip carrying a text label, so it is the\n\t// one worth making findable at a glance rather than another grey glyph in a row of grey\n\t// glyphs. A yellow #2 is about as legible as a small object gets.\n\t//\n\t// Chosen against a dark viewport: the graphite is a mid grey rather than near-black, because a\n\t// true graphite point disappears into the background exactly where the icon needs to read.\n\n\tprivate static readonly Color PencilBody = new( 0.96f, 0.76f, 0.15f );\n\tprivate static readonly Color PencilFerrule = new( 0.74f, 0.77f, 0.80f );\n\tprivate static readonly Color PencilEraser = new( 0.91f, 0.56f, 0.58f );\n\tprivate static readonly Color PencilWood = new( 0.87f, 0.68f, 0.44f );\n\tprivate static readonly Color PencilGraphite = new( 0.45f, 0.47f, 0.50f );\n\n\t/// \u003Csummary\u003EMultiplier applied to every coordinate, radius and pen width for the icon being\n\t/// drawn right now. Every glyph is authored against the nominal 18x18 box, so one factor set\n\t/// here at the top of Draw is enough to resize all of them together \u2014 the strip\u0027s buttons grew\n\t/// past the size the glyphs were drawn for and a fixed-size glyph in a big button reads as a\n\t/// mistake. Painting only ever happens on the editor UI thread, so a plain static is safe.\u003C/summary\u003E\n\tprivate static float _scale = 1f;\n\n\tpublic static void Draw( EffigyIcon icon, Vector2 center, Color color, float scale = 1f )\n\t{\n\t\tEditor.Paint.Antialiasing = true;\n\t\t_scale = scale;\n\n\t\tswitch ( icon )\n\t\t{\n\t\t\tcase EffigyIcon.Sketch: PaintSketch( center, color ); return;\n\t\t\tcase EffigyIcon.Primitive: PaintPrimitive( center, color ); return;\n\t\t\tcase EffigyIcon.Extrude: PaintExtrude( center, color ); return;\n\t\t\tcase EffigyIcon.Revolve: PaintRevolve( center, color ); return;\n\t\t\tcase EffigyIcon.Sweep: PaintSweep( center, color ); return;\n\t\t\tcase EffigyIcon.Loft: PaintLoft( center, color ); return;\n\t\t\tcase EffigyIcon.Chamfer: PaintChamfer( center, color ); return;\n\t\t\tcase EffigyIcon.Fillet: PaintFillet( center, color ); return;\n\t\t\tcase EffigyIcon.Shell: PaintShell( center, color ); return;\n\t\t\tcase EffigyIcon.Subdivide: PaintSubdivide( center, color ); return;\n\t\t\tcase EffigyIcon.Remesh: PaintRemesh( center, color ); return;\n\t\t\tcase EffigyIcon.Mirror: PaintMirror( center, color ); return;\n\t\t\tcase EffigyIcon.LinearPattern: PaintLinearPattern( center, color ); return;\n\t\t\tcase EffigyIcon.CircularPattern: PaintCircularPattern( center, color ); return;\n\t\t\tcase EffigyIcon.Transform: PaintTransform( center, color ); return;\n\t\t\tcase EffigyIcon.UVProject: PaintUVProject( center, color ); return;\n\t\t\tcase EffigyIcon.FaceMaterial: PaintFaceMaterial( center, color ); return;\n\t\t\tcase EffigyIcon.Boolean: PaintBoolean( center, color ); return;\n\t\t\tcase EffigyIcon.Plane: PaintPlane( center, color ); return;\n\n\t\t\tcase EffigyIcon.SelectTool: PaintSelectTool( center, color ); return;\n\t\t\tcase EffigyIcon.LineTool: PaintLineTool( center, color ); return;\n\t\t\tcase EffigyIcon.LineMidpointTool: PaintLineMidpointTool( center, color ); return;\n\t\t\tcase EffigyIcon.RectangleTool: PaintRectangleTool( center, color ); return;\n\t\t\tcase EffigyIcon.RectangleCentreTool: PaintRectangleCentreTool( center, color ); return;\n\t\t\tcase EffigyIcon.CircleTool: PaintCircleTool( center, color ); return;\n\t\t\tcase EffigyIcon.CircleThreePointTool: PaintCircleThreePointTool( center, color ); return;\n\t\t\tcase EffigyIcon.ArcTool: PaintArcTool( center, color ); return;\n\t\t\tcase EffigyIcon.ArcThreePointTool: PaintArcThreePointTool( center, color ); return;\n\t\t\tcase EffigyIcon.PolygonTool: PaintPolygonTool( center, color ); return;\n\t\t\tcase EffigyIcon.PolygonCircumscribedTool: PaintPolygonCircumscribedTool( center, color ); return;\n\t\t\tcase EffigyIcon.SlotTool: PaintSlotTool( center, color ); return;\n\t\t\tcase EffigyIcon.PointTool: PaintPointTool( center, color ); return;\n\t\t\tcase EffigyIcon.ConstructionTool: PaintConstructionTool( center, color ); return;\n\t\t\tcase EffigyIcon.ProfileInspectorTool: PaintProfileInspectorTool( center, color ); return;\n\t\t\tcase EffigyIcon.FinishSketchTool: PaintFinishSketchTool( center, color ); return;\n\n\t\t\tcase EffigyIcon.Sculpt: PaintSculpt( center, color ); return;\n\t\t\tcase EffigyIcon.SculptDraw: PaintSculptDraw( center, color ); return;\n\t\t\tcase EffigyIcon.SculptSmooth: PaintSculptSmooth( center, color ); return;\n\t\t\tcase EffigyIcon.SculptInflate: PaintSculptInflate( center, color ); return;\n\t\t\tcase EffigyIcon.SculptGrab: PaintSculptGrab( center, color ); return;\n\t\t\tcase EffigyIcon.SculptFlatten: PaintSculptFlatten( center, color ); return;\n\t\t\tcase EffigyIcon.SculptPinch: PaintSculptPinch( center, color ); return;\n\t\t\tcase EffigyIcon.SculptMask: PaintSculptMask( center, color ); return;\n\t\t\tcase EffigyIcon.SculptLevelDown: PaintSculptLevelDown( center, color ); return;\n\t\t\tcase EffigyIcon.SculptLevelUp: PaintSculptLevelUp( center, color ); return;\n\t\t\tcase EffigyIcon.SculptBake: PaintSculptBake( center, color ); return;\n\n\t\t\tcase EffigyIcon.Paint: PaintPaint( center, color ); return;\n\t\t\tcase EffigyIcon.PaintBrush: PaintPaintBrush( center, color ); return;\n\t\t\tcase EffigyIcon.PaintEraser: PaintPaintEraser( center, color ); return;\n\n\t\t\tcase EffigyIcon.Draft: PaintDraft( center, color ); return;\n\t\t\tcase EffigyIcon.MoveFace: PaintMoveFace( center, color ); return;\n\t\t\tcase EffigyIcon.Hole: PaintHole( center, color ); return;\n\n\t\t\tcase EffigyIcon.NoteTool: PaintNoteTool( center, color ); return;\n\t\t\tcase EffigyIcon.NoteEraseTool: PaintNoteEraseTool( center, color ); return;\n\n\t\t\tcase EffigyIcon.LightFullBright: PaintLightFullBright( center, color ); return;\n\t\t\tcase EffigyIcon.LightPoint: PaintLightPoint( center, color ); return;\n\t\t\tcase EffigyIcon.LightSpot: PaintLightSpot( center, color ); return;\n\t\t\tcase EffigyIcon.LightSun: PaintLightSun( center, color ); return;\n\t\t\tcase EffigyIcon.LightRigThreePoint: PaintLightRigThreePoint( center, color ); return;\n\t\t\tcase EffigyIcon.LightRigRim: PaintLightRigRim( center, color ); return;\n\t\t\tcase EffigyIcon.LightRigTop: PaintLightRigTop( center, color ); return;\n\t\t\tcase EffigyIcon.LightRigKey: PaintLightRigKey( center, color ); return;\n\t\t\tcase EffigyIcon.LightClear: PaintLightClear( center, color ); return;\n\n\t\t\tcase EffigyIcon.EllipseTool: PaintEllipseTool( center, color ); return;\n\t\t\tcase EffigyIcon.SplineTool: PaintSplineTool( center, color ); return;\n\t\t\tcase EffigyIcon.TrimTool: PaintTrimTool( center, color ); return;\n\t\t\tcase EffigyIcon.ExtendTool: PaintExtendTool( center, color ); return;\n\t\t\tcase EffigyIcon.SketchFilletTool: PaintSketchFilletTool( center, color ); return;\n\t\t\tcase EffigyIcon.OffsetTool: PaintOffsetTool( center, color ); return;\n\t\t\tcase EffigyIcon.MirrorTool: PaintMirrorTool( center, color ); return;\n\n\t\t\tcase EffigyIcon.UseTool: PaintUseTool( center, color ); return;\n\t\t\tcase EffigyIcon.UseAllTool: PaintUseAllTool( center, color ); return;\n\n\t\t\tcase EffigyIcon.MeshEdit: PaintMeshEdit( center, color ); return;\n\t\t\tcase EffigyIcon.SelectVertex: PaintCage( center, color, lit: 1 ); return;\n\t\t\tcase EffigyIcon.SelectEdge: PaintCage( center, color, lit: 2 ); return;\n\t\t\tcase EffigyIcon.SelectFace: PaintCage( center, color, lit: 3 ); return;\n\t\t\tcase EffigyIcon.Inset: PaintInset( center, color ); return;\n\t\t\tcase EffigyIcon.LoopCut: PaintLoopCut( center, color ); return;\n\t\t\tcase EffigyIcon.Merge: PaintMerge( center, color ); return;\n\t\t\tcase EffigyIcon.Dissolve: PaintDissolve( center, color ); return;\n\t\t\tcase EffigyIcon.DeleteGeometry: PaintDeleteGeometry( center, color ); return;\n\t\t\tcase EffigyIcon.Shrinkwrap: PaintShrinkwrap( center, color ); return;\n\t\t\tcase EffigyIcon.Solidify: PaintSolidify( center, color ); return;\n\t\t\tcase EffigyIcon.Drape: PaintDrape( center, color ); return;\n\t\t\tcase EffigyIcon.Fabric: PaintFabric( center, color ); return;\n\t\t\tcase EffigyIcon.Seam: PaintSeam( center, color ); return;\n\t\t\tcase EffigyIcon.Unwrap: PaintUnwrap( center, color ); return;\n\t\t\tcase EffigyIcon.Wearer: PaintWearer( center, color ); return;\n\t\t\tcase EffigyIcon.Frill: PaintFrill( center, color ); return;\n\n\t\t\tcase EffigyIcon.CutTool: PaintCutTool( center, color ); return;\n\n\t\t\tcase EffigyIcon.Bone: PaintBone( center, color ); return;\n\t\t\tcase EffigyIcon.BoneBind: PaintBoneBind( center, color ); return;\n\t\t\tcase EffigyIcon.BoneSoft: PaintBoneSoft( center, color ); return;\n\t\t\tcase EffigyIcon.SoftPreview: PaintSoftPreview( center, color ); return;\n\t\t\tcase EffigyIcon.SoftRest: PaintSoftRest( center, color ); return;\n\t\t}\n\t}\n\n\t// --- drawing helpers --------------------------------------------------------------------\n\n\tprivate static void Stroked( Color color, float width = Stroke )\n\t{\n\t\tEditor.Paint.ClearBrush();\n\t\tEditor.Paint.SetPen( color, width * _scale );\n\t}\n\n\tprivate static void Filled( Color color )\n\t{\n\t\tEditor.Paint.ClearPen();\n\t\tEditor.Paint.SetBrush( color );\n\t}\n\n\t/// \u003Csummary\u003EClosed outline through the given points \u2014 DrawPolygon fills, so an outlined shape\n\t/// has to be walked as lines.\u003C/summary\u003E\n\tprivate static void Outline( params Vector2[] points )\n\t{\n\t\tfor ( var i = 0; i \u003C points.Length; i\u002B\u002B )\n\t\t\tEditor.Paint.DrawLine( points[i], points[(i \u002B 1) % points.Length] );\n\t}\n\n\t/// \u003Csummary\u003EAn arc as a polyline. There is no arc primitive in Paint, and approximating with\n\t/// segments is exact enough at icon size.\u003C/summary\u003E\n\tprivate static void Arc( Vector2 center, float radius, float fromDegrees, float toDegrees, int segments = 14 )\n\t{\n\t\tvar previous = Vector2.Zero;\n\n\t\tfor ( var i = 0; i \u003C= segments; i\u002B\u002B )\n\t\t{\n\t\t\tvar t = fromDegrees \u002B (toDegrees - fromDegrees) * (i / (float)segments);\n\t\t\tvar radians = t * MathF.PI / 180f;\n\t\t\tvar point = center \u002B new Vector2( MathF.Cos( radians ) * radius, MathF.Sin( radians ) * radius ) * _scale;\n\n\t\t\tif ( i \u003E 0 )\n\t\t\t\tEditor.Paint.DrawLine( previous, point );\n\n\t\t\tprevious = point;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EAn elliptical arc as a polyline. Arc() draws a circle and cannot say \u0022this circle\n\t/// is lying flat\u0022; that foreshortening is the entire difference between a glyph that reads as\n\t/// a rotation about an axis and one that reads as a spiral.\u003C/summary\u003E\n\tprivate static void EllipseArc( Vector2 center, float radiusX, float radiusY,\n\t\tfloat fromDegrees, float toDegrees, int segments = 24 )\n\t{\n\t\tvar previous = Vector2.Zero;\n\n\t\tfor ( var i = 0; i \u003C= segments; i\u002B\u002B )\n\t\t{\n\t\t\tvar t = fromDegrees \u002B (toDegrees - fromDegrees) * (i / (float)segments);\n\t\t\tvar radians = t * MathF.PI / 180f;\n\t\t\tvar point = center \u002B new Vector2( MathF.Cos( radians ) * radiusX, MathF.Sin( radians ) * radiusY ) * _scale;\n\n\t\t\tif ( i \u003E 0 )\n\t\t\t\tEditor.Paint.DrawLine( previous, point );\n\n\t\t\tprevious = point;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EA solid triangular arrow head, pointing along \u003Cparamref name=\u0022direction\u0022/\u003E.\u003C/summary\u003E\n\tprivate static void ArrowHead( Vector2 tip, Vector2 direction, Color color, float size = 3.4f )\n\t{\n\t\tvar d = direction.Normal;\n\t\tvar side = new Vector2( -d.y, d.x );\n\n\t\tsize *= _scale;\n\n\t\tFilled( color );\n\t\tEditor.Paint.DrawPolygon(\n\t\t\ttip,\n\t\t\ttip - d * size \u002B side * size * 0.62f,\n\t\t\ttip - d * size - side * size * 0.62f );\n\t}\n\n\tprivate static Vector2 At( Vector2 center, float x, float y ) =\u003E center \u002B new Vector2( x, y ) * _scale;\n\n\t/// \u003Csummary\u003EA rect in the same nominal icon space At() uses, for the glyphs that need\n\t/// DrawRect/DrawCircle rather than a walked outline.\u003C/summary\u003E\n\tprivate static Rect Box( Vector2 center, float x, float y, float width, float height )\n\t\t=\u003E new Rect( center.x \u002B x * _scale, center.y \u002B y * _scale, width * _scale, height * _scale );\n\n\t// --- the icons --------------------------------------------------------------------------\n\n\t/// \u003Csummary\u003E\n\t/// A pencil drawing on a sheet, its point resting ON the paper\u0027s top edge.\n\t///\n\t/// The pencil used to be a plain parallelogram - blunt at both ends, with one of its corners\n\t/// landing on the paper line. A pencil reads as a pencil because of the cone at the end, and\n\t/// the mark reads as DRAWING because that cone touches the paper rather than hovering above\n\t/// it or crossing through it. So: a solid tapered point that ends exactly on the line, an\n\t/// outlined barrel behind it, and a band where the ferrule would be.\n\t///\n\t/// Every coordinate is derived from the tip and the pencil\u0027s axis, laid out along a 45 degree\n\t/// diagonal, so the point cannot drift off the paper if the proportions are adjusted.\n\t///\n\t/// The paper keeps the colour it is handed; the pencil does not (see PencilBody and friends).\n\t/// \u003C/summary\u003E\n\tprivate static void PaintSketch( Vector2 c, Color color )\n\t{\n\t\t// Paper: a single flat horizontal line, matching the compact reference glyph. Everything\n\t\t// else is placed against PaperY.\n\t\tconst float PaperY = 4.8f;\n\n\t\t// How thick the pencil is, across the barrel.\n\t\tconst float BarrelWidth = 1.7f;\n\n\t\tStroked( color, Stroke );\n\t\tEditor.Paint.DrawLine( At( c, -6.6f, PaperY ), At( c, 6.4f, PaperY ) );\n\n\t\t// The sharpened cone, sitting on the paper. Solid, because at 27px a hollow cone is a\n\t\t// smudge - the filled wedge is what makes it read as sharpened. Its base is exactly as\n\t\t// wide as the barrel\u0027s stroke, so the two meet without a step.\n\t\tFilled( PencilWood );\n\t\tEditor.Paint.DrawPolygon(\n\t\t\tAt( c, -4.4f, PaperY ),\n\t\t\tAt( c, -1.678f, 3.28f ),\n\t\t\tAt( c, -2.88f, 2.078f ) );\n\n\t\t// The exposed lead, the outer 40% of that cone. Drawn over the wood rather than beside it,\n\t\t// so the two always agree about where the point is.\n\t\tFilled( PencilGraphite );\n\t\tEditor.Paint.DrawPolygon(\n\t\t\tAt( c, -4.4f, PaperY ),\n\t\t\tAt( c, -3.257f, 4.161f ),\n\t\t\tAt( c, -3.761f, 3.657f ) );\n\n\t\t// Barrel: ONE STROKED LINE, not an outlined shape. A pencil this slim has a body 1.7 units\n\t\t// across, and two outline strokes inside that merge into a blob - the line IS the barrel,\n\t\t// and it is the only way to get a thin pencil that still reads at icon size. The ferrule\n\t\t// and eraser are further stretches of the same line, which is also why they cannot drift\n\t\t// out of alignment with it.\n\t\tStroked( PencilBody, BarrelWidth );\n\t\tEditor.Paint.DrawLine( At( c, -2.279f, 2.679f ), At( c, 4.156f, -3.756f ) );\n\n\t\tStroked( PencilFerrule, BarrelWidth );\n\t\tEditor.Paint.DrawLine( At( c, 4.156f, -3.756f ), At( c, 4.948f, -4.548f ) );\n\n\t\tStroked( PencilEraser, BarrelWidth );\n\t\tEditor.Paint.DrawLine( At( c, 4.948f, -4.548f ), At( c, 5.853f, -5.453f ) );\n\t}\n\n\t/// \u003Csummary\u003EAn isometric cube \u2014 the generic \u0022a solid body\u0022 mark.\u003C/summary\u003E\n\tprivate static void PaintPrimitive( Vector2 c, Color color )\n\t{\n\t\tStroked( color );\n\t\tOutline(\n\t\t\tAt( c, 0, -7.5f ), At( c, 7, -3.6f ), At( c, 7, 3.6f ),\n\t\t\tAt( c, 0, 7.5f ), At( c, -7, 3.6f ), At( c, -7, -3.6f ) );\n\n\t\t// The three edges meeting at the near corner are what make it read as a cube rather than\n\t\t// a hexagon.\n\t\tEditor.Paint.DrawLine( At( c, 0, 0 ), At( c, 0, 7.5f ) );\n\t\tEditor.Paint.DrawLine( At( c, 0, 0 ), At( c, 7, -3.6f ) );\n\t\tEditor.Paint.DrawLine( At( c, 0, 0 ), At( c, -7, -3.6f ) );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A profile lying flat, and the solid pulled UP off it.\n\t///\n\t/// The old glyph had the profile on top with the arrow pointing down, which reads as something\n\t/// falling rather than as something being drawn out \u2014 and at toolbar size the whole thing came\n\t/// out looking like a plumb bob. Arrow and profile now agree about which way an extrude goes.\n\t/// \u003C/summary\u003E\n\tprivate static void PaintExtrude( Vector2 c, Color color )\n\t{\n\t\t// The sketch, in plan, dimmed: it is what the operation starts FROM, not what it makes.\n\t\tStroked( color.WithAlpha( 0.55f ), 1.5f );\n\t\tOutline( At( c, -7.5f, 6.5f ), At( c, 0, 9.4f ), At( c, 7.5f, 6.5f ), At( c, 0, 3.6f ) );\n\n\t\tStroked( color, 2.6f );\n\t\tEditor.Paint.DrawLine( At( c, 0, 6.5f ), At( c, 0, -4.2f ) );\n\n\t\tArrowHead( At( c, 0, -8.6f ), new Vector2( 0, -1 ), color, 4.2f );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A sketch sitting on an axis, and the spin that turns it into a solid.\n\t///\n\t/// Extrude is a straight arrow off a profile. This is the same grammar bent into a C \u2014 axis,\n\t/// profile, curved arrow \u2014 which is what every CAD tool draws for Revolve and what the last\n\t/// version threw out. That version drew a vase in section and hoped the silhouette would\n\t/// carry it; at toolbar size it was a lumpy outline with a dashed line through its face.\n\t/// Fill the profile (same weight as Chamfer and Shell) and let the arrow be the operation.\n\t/// \u003C/summary\u003E\n\tprivate static void PaintRevolve( Vector2 c, Color color )\n\t{\n\t\t// ONSHAPE\u0027S OWN REVOLVE ICON: a disc with a wedge taken out of it. po pointed at it, and it\n\t\t// is better than either drawing that came before.\n\t\t//\n\t\t// The first attempt stacked a dashed axis, a filled profile rectangle and a circular arrow\n\t\t// on top of each other; the arc swept straight through the rectangle it was meant to be\n\t\t// spinning and the three merged into a tall dark blob that read as the letter D. The\n\t\t// second replaced it with a lathe - profile beside an axis, ellipse sweeping round it -\n\t\t// which was legible but was three small shapes doing the work of one, and small shapes are\n\t\t// what stop reading first when the strip is the only chrome on a 3D viewport.\n\t\t//\n\t\t// A disc with a mouth is ONE shape. It shows the result rather than the mechanism, which\n\t\t// is the idiom the rest of this strip already uses - Chamfer cuts a corner off a square,\n\t\t// Shell puts a wall inside one. The mouth is what makes it a revolve rather than a circle:\n\t\t// the two cut faces meeting at the centre are the start and end of the sweep, and they say\n\t\t// \u0022this was swept through an angle\u0022 without needing an arrow to explain it. It is also\n\t\t// square and solid, so it holds its weight next to Extrude and Loft instead of being the\n\t\t// one thin glyph on the row.\n\n\t\tconst float Radius = 8.6f;\n\n\t\t// How much of the disc is missing. Wide enough to read as a deliberate mouth at 32px\n\t\t// rather than as a nick in the outline, narrow enough that the shape is still a disc.\n\t\tconst float MouthHalfAngle = 38f;\n\n\t\t// The rim, from one cut face round to the other, then in to the centre. Closing the loop\n\t\t// draws the second cut face, so the whole silhouette is one walk.\n\t\tvar rim = ArcPoints( c, Radius, MouthHalfAngle, 360f - MouthHalfAngle, 30 );\n\t\trim.Add( c );\n\n\t\tvar silhouette = rim.ToArray();\n\n\t\tFilled( color.WithAlpha( 0.22f ) );\n\t\tEditor.Paint.DrawPolygon( silhouette );\n\n\t\tStroked( color, 1.6f );\n\t\tOutline( silhouette );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A profile carried along a path, with the path drawn as the thing that shapes it.\n\t///\n\t/// Sweep and Extrude are the same sentence with a different verb \u2014 a profile, and where it goes\n\t/// \u2014 so they are drawn with the same grammar: the starting profile dim because it is what the\n\t/// operation begins FROM rather than what it makes, and an arrow for the operation itself. The\n\t/// difference between them is the whole point, so here the path is a curve and the solid\n\t/// follows it instead of standing straight up.\n\t/// \u003C/summary\u003E\n\tprivate static void PaintSweep( Vector2 c, Color color )\n\t{\n\t\t// Hub, radius and extent of the path. Everything else is derived from these, so the glyph\n\t\t// stays consistent with itself if the arc is retuned.\n\t\tconst float HubX = -7f;\n\t\tconst float HubY = -7.5f;\n\t\tconst float Radius = 12f;\n\n\t\t// Half the profile\u0027s width, so the band either side of the path IS the solid.\n\t\tconst float Half = 2.6f;\n\n\t\tconst float From = 0f;\n\t\tconst float To = 90f;\n\t\tconst float ArrowAt = To \u002B 10f;\n\n\t\tvar hub = At( c, HubX, HubY );\n\n\t\t// The swept solid: the path offset either side of itself. Faint rather than outlined at\n\t\t// full weight, so the path stays the strongest line in the glyph.\n\t\tStroked( color.WithAlpha( 0.32f ), 2f );\n\t\tArc( hub, Radius - Half, From, To, 18 );\n\t\tArc( hub, Radius \u002B Half, From, To, 18 );\n\n\t\tStroked( color, 1.7f );\n\t\tArc( hub, Radius, From, ArrowAt, 20 );\n\n\t\t// Where it starts, and where it arrives.\n\t\tSweepStation( hub, Radius, Half, From, color.WithAlpha( 0.55f ), 1.4f );\n\t\tSweepStation( hub, Radius, Half, To, color, 1.7f );\n\n\t\tvar end = ArrowAt * MathF.PI / 180f;\n\t\tvar tip = hub \u002B new Vector2( MathF.Cos( end ), MathF.Sin( end ) ) * Radius * _scale;\n\n\t\tArrowHead( tip, new Vector2( -MathF.Sin( end ), MathF.Cos( end ) ), color, 3.6f );\n\t}\n\n\t/// \u003Csummary\u003EThe profile at one station of a sweep: a diamond spanning the swept band, drawn\n\t/// ACROSS the path rather than lying flat, because a sweep takes its profile perpendicular to\n\t/// where it is going \u2014 see SweepFeature.\u003C/summary\u003E\n\tprivate static void SweepStation( Vector2 hub, float radius, float half, float degrees, Color color, float width )\n\t{\n\t\tvar angle = degrees * MathF.PI / 180f;\n\t\tvar radial = new Vector2( MathF.Cos( angle ), MathF.Sin( angle ) );\n\t\tvar tangent = new Vector2( -radial.y, radial.x );\n\t\tvar centre = hub \u002B radial * radius * _scale;\n\n\t\tStroked( color, width );\n\t\tOutline(\n\t\t\tcentre \u002B radial * half * _scale,\n\t\t\tcentre \u002B tangent * half * 0.62f * _scale,\n\t\t\tcentre - radial * half * _scale,\n\t\t\tcentre - tangent * half * 0.62f * _scale );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Two sections, and the skin ruled between them.\n\t///\n\t/// The sections are drawn as flat diamonds \u2014 the same plan-view profile Extrude and Sweep use,\n\t/// so a closed sketch reads the same way everywhere on this strip \u2014 one small and one large, so\n\t/// what lies between them has to be a loft rather than an extrusion. The sides are STRAIGHT,\n\t/// which is what the kernel actually does: neighbouring sections joined by a ruled surface, not\n\t/// a spline smoothly through them.\n\t/// \u003C/summary\u003E\n\tprivate static void PaintLoft( Vector2 c, Color color )\n\t{\n\t\tconst float TopY = -7f;\n\t\tconst float TopHalf = 3.4f;\n\t\tconst float BottomY = 6.6f;\n\t\tconst float BottomHalf = 7.6f;\n\n\t\t// The skin, as a tint between the two sections \u2014 same weight as Chamfer and Shell, so it\n\t\t// reads as material rather than as two more lines.\n\t\tFilled( color.WithAlpha( 0.22f ) );\n\t\tEditor.Paint.DrawPolygon(\n\t\t\tAt( c, -TopHalf, TopY ), At( c, TopHalf, TopY ),\n\t\t\tAt( c, BottomHalf, BottomY ), At( c, -BottomHalf, BottomY ) );\n\n\t\tStroked( color, 1.7f );\n\t\tEditor.Paint.DrawLine( At( c, -TopHalf, TopY ), At( c, -BottomHalf, BottomY ) );\n\t\tEditor.Paint.DrawLine( At( c, TopHalf, TopY ), At( c, BottomHalf, BottomY ) );\n\n\t\tLoftSection( c, TopY, TopHalf, TopHalf * 0.46f, color );\n\t\tLoftSection( c, BottomY, BottomHalf, BottomHalf * 0.32f, color );\n\t}\n\n\t/// \u003Csummary\u003EOne loft section, in plan: a diamond as wide as the section and shallow enough to\n\t/// read as lying flat, rather than as the top and bottom edges of a trapezium.\u003C/summary\u003E\n\tprivate static void LoftSection( Vector2 c, float y, float half, float depth, Color color )\n\t{\n\t\tStroked( color, 1.5f );\n\t\tOutline( At( c, -half, y ), At( c, 0, y - depth ), At( c, half, y ), At( c, 0, y \u002B depth ) );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A solid block with its corner cut away, the cut face called out.\n\t///\n\t/// The old glyph was an outlined square with a small nick in one corner, which at toolbar size\n\t/// is a page icon and nothing else. Two changes fix it: fill the body, so it reads as a solid\n\t/// rather than as a sheet, and cut deep enough that the chamfer is a face rather than a nick.\n\t/// The faint lines show the corner that was removed.\n\t/// \u003C/summary\u003E\n\tprivate static void PaintChamfer( Vector2 c, Color color )\n\t{\n\t\tFilled( color.WithAlpha( 0.22f ) );\n\t\tEditor.Paint.DrawPolygon(\n\t\t\tAt( c, -7, -1.5f ), At( c, -1.5f, -7 ), At( c, 7, -7 ), At( c, 7, 7 ), At( c, -7, 7 ) );\n\n\t\tStroked( color, 1.5f );\n\t\tOutline( At( c, -7, -1.5f ), At( c, -1.5f, -7 ), At( c, 7, -7 ), At( c, 7, 7 ), At( c, -7, 7 ) );\n\n\t\t// The cut face, in the same amber the sketch pencil draws with \u2014 the accent on this strip\n\t\t// means \u0022the thing this operation did\u0022.\n\t\tStroked( ClickColor, 2.8f );\n\t\tEditor.Paint.DrawLine( At( c, -7, -1.5f ), At( c, -1.5f, -7 ) );\n\n\t\tStroked( color.WithAlpha( 0.3f ), 1f );\n\t\tEditor.Paint.DrawLine( At( c, -7, -1.5f ), At( c, -7, -7 ) );\n\t\tEditor.Paint.DrawLine( At( c, -7, -7 ), At( c, -1.5f, -7 ) );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The chamfer\u0027s twin, and deliberately so: the same solid, the same corner gone, the same\n\t/// ghost of the corner that was removed \u2014 the ONLY difference is that the accent is an arc\n\t/// instead of a straight line.\n\t///\n\t/// That is the whole point. These two sit next to each other on the strip and the thing a\n\t/// person needs to tell apart at 40px is round versus flat, which a shared body makes obvious\n\t/// and two unrelated drawings would bury.\n\t/// \u003C/summary\u003E\n\tprivate static void PaintFillet( Vector2 c, Color color )\n\t{\n\t\t// The arc\u0027s centre is the inner corner of the cut, so it runs from (-7,-1.5) to (-1.5,-7)\n\t\t// exactly where the chamfer\u0027s straight cut does.\n\t\tvar arc = ArcPoints( At( c, -1.5f, -1.5f ), 5.5f, 180f, 270f, 10 );\n\n\t\tvar body = new List\u003CVector2\u003E( arc );\n\t\tbody.Add( At( c, 7, -7 ) );\n\t\tbody.Add( At( c, 7, 7 ) );\n\t\tbody.Add( At( c, -7, 7 ) );\n\n\t\tFilled( color.WithAlpha( 0.22f ) );\n\t\tEditor.Paint.DrawPolygon( body.ToArray() );\n\n\t\tStroked( color, 1.5f );\n\t\tOutline( body.ToArray() );\n\n\t\tStroked( ClickColor, 2.8f );\n\t\tArc( At( c, -1.5f, -1.5f ), 5.5f, 180f, 270f, 10 );\n\n\t\tStroked( color.WithAlpha( 0.3f ), 1f );\n\t\tEditor.Paint.DrawLine( At( c, -7, -1.5f ), At( c, -7, -7 ) );\n\t\tEditor.Paint.DrawLine( At( c, -7, -7 ), At( c, -1.5f, -7 ) );\n\t}\n\n\t/// \u003Csummary\u003EThe points Arc walks, for a glyph that needs the arc as part of a filled outline\n\t/// rather than as a stroke. Same maths, so the fill and the stroke cannot drift apart.\u003C/summary\u003E\n\tprivate static List\u003CVector2\u003E ArcPoints( Vector2 center, float radius,\n\t\tfloat fromDegrees, float toDegrees, int segments )\n\t{\n\t\tvar points = new List\u003CVector2\u003E( segments \u002B 1 );\n\n\t\tfor ( var i = 0; i \u003C= segments; i\u002B\u002B )\n\t\t{\n\t\t\tvar t = fromDegrees \u002B (toDegrees - fromDegrees) * (i / (float)segments);\n\t\t\tvar radians = t * MathF.PI / 180f;\n\n\t\t\tpoints.Add( center \u002B new Vector2( MathF.Cos( radians ) * radius, MathF.Sin( radians ) * radius ) * _scale );\n\t\t}\n\n\t\treturn points;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A hollowed solid in section: material on three sides, opening at the top.\n\t///\n\t/// A square inside a square is a frame, a border, a picture \u2014 it was never going to say\n\t/// \u0022hollowed to a wall thickness\u0022. THE WALL IS THE OBJECT, so the wall is what gets filled and\n\t/// the void is what gets left out, which is how a section drawing says it.\n\t/// \u003C/summary\u003E\n\t/// \u003Csummary\u003E\n\t/// Two overlapping squares with the shared region solid \u2014 a boolean, drawn as what it operates\n\t/// on rather than as which of the three it is.\n\t///\n\t/// ONE GLYPH FOR ALL THREE OPERATIONS, because the button carries a dropdown and the variant\n\t/// chosen is named on it. A Venn lens is the universal mark for this and reads at twenty-four\n\t/// pixels; three near-identical lenses differing only in which part is filled do not, and the\n\t/// one thing worse than an icon you have to think about is three you have to tell apart.\n\t///\n\t/// SQUARES RATHER THAN CIRCLES so the overlap is exact: the intersection of two axis-aligned\n\t/// rectangles is a rectangle, which DrawRect can fill honestly. Two circles would need the lens\n\t/// approximated by a polygon, and at this size the approximation is what you would see.\n\t/// \u003C/summary\u003E\n\tprivate static void PaintBoolean( Vector2 c, Color color )\n\t{\n\t\t// The shared volume first, so the outlines drawn over it keep their edges crisp.\n\t\tFilled( color.WithAlpha( 0.9f ) );\n\t\tEditor.Paint.DrawRect( Box( c, -2f, -3f, 4f, 6f ), 0.8f * _scale );\n\n\t\tStroked( color.WithAlpha( 0.85f ), 1.2f );\n\t\tEditor.Paint.DrawRect( Box( c, -7f, -6f, 9f, 9f ), 1.4f * _scale );\n\t\tEditor.Paint.DrawRect( Box( c, -2f, -3f, 9f, 9f ), 1.4f * _scale );\n\t}\n\n\tprivate static void PaintShell( Vector2 c, Color color )\n\t{\n\t\tFilled( color.WithAlpha( 0.9f ) );\n\t\tEditor.Paint.DrawPolygon(\n\t\t\tAt( c, -7.8f, -7f ), At( c, -3.6f, -7f ), At( c, -3.6f, 3.2f ),\n\t\t\tAt( c, 3.6f, 3.2f ), At( c, 3.6f, -7f ), At( c, 7.8f, -7f ),\n\t\t\tAt( c, 7.8f, 7.4f ), At( c, -7.8f, 7.4f ) );\n\n\t\t// The opening, as a faint lid line, so the U reads as a container rather than as a letter.\n\t\tStroked( color.WithAlpha( 0.45f ), 1.1f );\n\t\tEditor.Paint.DrawLine( At( c, -3.6f, -7f ), At( c, 3.6f, -7f ) );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A quad split into four, with one of those four split again \u2014 subdivision, drawn literally.\n\t///\n\t/// The old glyph was a rounded square with a cross and a dot in the middle, which is the\n\t/// universal \u0022add\u0022 icon and was read as one. Showing one quadrant DENSER than its neighbours is\n\t/// what the operation actually does, and the density is carried by a tint as well as by lines so\n\t/// it survives being small \u2014 at twenty-four pixels a 4x4 of hairlines is a grey smear.\n\t/// \u003C/summary\u003E\n\tprivate static void PaintSubdivide( Vector2 c, Color color )\n\t{\n\t\tStroked( color, 1.6f );\n\t\tOutline( At( c, -8, -8 ), At( c, 8, -8 ), At( c, 8, 8 ), At( c, -8, 8 ) );\n\n\t\tFilled( color.WithAlpha( 0.3f ) );\n\t\tEditor.Paint.DrawPolygon( At( c, -8, -8 ), At( c, 0, -8 ), At( c, 0, 0 ), At( c, -8, 0 ) );\n\n\t\tStroked( color.WithAlpha( 0.9f ), 1.5f );\n\t\tEditor.Paint.DrawLine( At( c, 0, -8 ), At( c, 0, 8 ) );\n\t\tEditor.Paint.DrawLine( At( c, -8, 0 ), At( c, 8, 0 ) );\n\n\t\tStroked( color.WithAlpha( 0.85f ), 1.2f );\n\t\tEditor.Paint.DrawLine( At( c, -4, -8 ), At( c, -4, 0 ) );\n\t\tEditor.Paint.DrawLine( At( c, -8, -4 ), At( c, 0, -4 ) );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The Subdivide glyph read backwards: a faint fine grid, and a bold coarse one over it.\n\t///\n\t/// DELIBERATELY THE SAME SQUARE. The two operations are the same axis in opposite directions,\n\t/// and a button that looked unrelated to Subdivide would hide that \u2014 they sit next to each other\n\t/// on two bars. What tells them apart is which grid is emphasised: Subdivide draws the fine\n\t/// lines solid because those are the ones it is adding, and this draws them faint because those\n\t/// are the ones going away.\n\t/// \u003C/summary\u003E\n\tprivate static void PaintRemesh( Vector2 c, Color color )\n\t{\n\t\tStroked( color, 1.6f );\n\t\tOutline( At( c, -8, -8 ), At( c, 8, -8 ), At( c, 8, 8 ), At( c, -8, 8 ) );\n\n\t\t// What is being lost.\n\t\tStroked( color.WithAlpha( 0.28f ), 1f );\n\t\tfor ( var i = -4f; i \u003C 8f; i \u002B= 4f )\n\t\t{\n\t\t\tEditor.Paint.DrawLine( At( c, i, -8 ), At( c, i, 8 ) );\n\t\t\tEditor.Paint.DrawLine( At( c, -8, i ), At( c, 8, i ) );\n\t\t}\n\n\t\t// What is left: one diagonal across the whole square, so it reads as two big triangles\n\t\t// rather than as a coarser grid of the same thing.\n\t\tStroked( color, 1.8f );\n\t\tEditor.Paint.DrawLine( At( c, -8, 8 ), At( c, 8, -8 ) );\n\t}\n\n\t/// \u003Csummary\u003EA solid shape and its reflection across a dashed mirror line.\u003C/summary\u003E\n\tprivate static void PaintMirror( Vector2 c, Color color )\n\t{\n\t\t// Mirror plane, dashed.\n\t\tStroked( color.WithAlpha( 0.5f ), 1.2f );\n\t\tfor ( var y = -8f; y \u003C 8f; y \u002B= 3.6f )\n\t\t\tEditor.Paint.DrawLine( At( c, 0, y ), At( c, 0, y \u002B 2.1f ) );\n\n\t\t// Source: solid.\n\t\tFilled( color );\n\t\tEditor.Paint.DrawPolygon( At( c, -2.4f, -5.6f ), At( c, -8, 0 ), At( c, -2.4f, 5.6f ) );\n\n\t\t// Reflection: outlined, so the two are not mistaken for a pattern.\n\t\tStroked( color );\n\t\tOutline( At( c, 2.4f, -5.6f ), At( c, 8, 0 ), At( c, 2.4f, 5.6f ) );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A plane placed clear of the one it came from: a faint parallelogram below, a solid one above,\n\t/// and the gap between them called out.\n\t///\n\t/// TWO OF THEM, NOT ONE. A single parallelogram is what \u0022face\u0022 or \u0022surface\u0022 looks like in every\n\t/// icon set there is, and this tool is not about a plane \u2014 it is about a plane\u0027s RELATIONSHIP to\n\t/// something else. The pair says offset without a caption, and the arrow between them says which\n\t/// of the two is the new one.\n\t/// \u003C/summary\u003E\n\tprivate static void PaintPlane( Vector2 c, Color color )\n\t{\n\t\t// The reference: faint, because it is what you already had.\n\t\tStroked( color.WithAlpha( 0.4f ) );\n\t\tOutline( Parallelogram( c, 3.9f ) );\n\n\t\t// The new plane, filled just enough to read as a surface rather than as a wire loop.\n\t\tFilled( color.WithAlpha( 0.18f ) );\n\t\tEditor.Paint.DrawPolygon( Parallelogram( c, -3.9f ) );\n\n\t\tStroked( color );\n\t\tOutline( Parallelogram( c, -3.9f ) );\n\n\t\t// The offset itself, pointing at the plane that was made rather than the one it came from.\n\t\tStroked( color.WithAlpha( 0.75f ), 1.2f );\n\t\tEditor.Paint.DrawLine( At( c, 0, 3.2f ), At( c, 0, -2.2f ) );\n\t\tArrowHead( At( c, 0, -3.2f ), new Vector2( 0, -1 ), color.WithAlpha( 0.75f ), 2.6f );\n\t}\n\n\t/// \u003Csummary\u003EA plane in three-quarter view, centred on \u003Cparamref name=\u0022y\u0022/\u003E. Shared by the two\n\t/// halves of the Plane glyph so they cannot drift into different shapes.\u003C/summary\u003E\n\tprivate static Vector2[] Parallelogram( Vector2 c, float y )\n\t{\n\t\tconst float Half = 5.4f;\n\t\tconst float SkewX = 2.5f;\n\t\tconst float SkewY = 2.2f;\n\n\t\treturn new[]\n\t\t{\n\t\t\tAt( c, -Half \u002B SkewX, y - SkewY ),\n\t\t\tAt( c, Half \u002B SkewX, y - SkewY ),\n\t\t\tAt( c, Half - SkewX, y \u002B SkewY ),\n\t\t\tAt( c, -Half - SkewX, y \u002B SkewY ),\n\t\t};\n\t}\n\n\t/// \u003Csummary\u003EOne body copied along a direction \u2014 first solid, copies outlined and fading.\u003C/summary\u003E\n\tprivate static void PaintLinearPattern( Vector2 c, Color color )\n\t{\n\t\t// TWO FAULTS, AND THE SECOND IS THE ONE THAT MATTERED. Three 6-unit squares starting at\n\t\t// -8.4 ran to \u002B12, so the glyph was 20.4 units wide in a box of 18 - and, worse, its centre\n\t\t// of mass sat 1.8 units RIGHT of the button\u0027s centre, because the run was never balanced\n\t\t// about it. An off-centre glyph in a row of centred ones is visible long before an\n\t\t// oversized one is, and neither is visible while looking at this icon on its own.\n\t\t//\n\t\t// Three 5.2 squares with 1.2 between them is 18 exactly, laid out symmetrically about c.\n\t\tconst float Square = 5.2f;\n\t\tconst float Gap = 1.2f;\n\t\tconst float First = -9f;\n\n\t\tFilled( color );\n\t\tEditor.Paint.DrawRect( Box( c, First, -Square / 2f, Square, Square ), 1.2f * _scale );\n\n\t\tStroked( color.WithAlpha( 0.8f ) );\n\t\tEditor.Paint.DrawRect( Box( c, First \u002B Square \u002B Gap, -Square / 2f, Square, Square ), 1.2f * _scale );\n\n\t\tStroked( color.WithAlpha( 0.45f ) );\n\t\tEditor.Paint.DrawRect( Box( c, First \u002B 2f * (Square \u002B Gap), -Square / 2f, Square, Square ), 1.2f * _scale );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Copies stepped around an axis, on a ring that can actually be seen.\n\t///\n\t/// The old glyph drew the ring as twelve dashes, and at toolbar size twelve dashes are a faint\n\t/// smudge \u2014 which left three small squares floating with nothing to explain them. A solid thin\n\t/// ring and a dot at the centre cost less ink and say more, and the copies are smaller than they\n\t/// were so they sit ON the ring instead of swallowing it.\n\t/// \u003C/summary\u003E\n\tprivate static void PaintCircularPattern( Vector2 c, Color color )\n\t{\n\t\tStroked( color.WithAlpha( 0.7f ), 1.4f );\n\t\tArc( c, 6.6f, 0f, 360f, 40 );\n\n\t\tFilled( color.WithAlpha( 0.8f ) );\n\t\tEditor.Paint.DrawRect( Box( c, -1.3f, -1.3f, 2.6f, 2.6f ), 1.3f * _scale );\n\n\t\tvar angles = new[] { -90f, 30f, 150f };\n\n\t\tfor ( var i = 0; i \u003C angles.Length; i\u002B\u002B )\n\t\t{\n\t\t\tvar radians = angles[i] * MathF.PI / 180f;\n\n\t\t\tvar box = Box( c,\n\t\t\t\tMathF.Cos( radians ) * 6.6f - 2.5f,\n\t\t\t\tMathF.Sin( radians ) * 6.6f - 2.5f, 5f, 5f );\n\n\t\t\t// One filled, the rest outlined \u2014 the same \u0022this is the original, these are the copies\u0022\n\t\t\t// grammar the linear pattern uses, so the pair read as a family.\n\t\t\tif ( i == 0 )\n\t\t\t{\n\t\t\t\tFilled( color );\n\t\t\t\tEditor.Paint.DrawRect( box, 1f * _scale );\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tStroked( color.WithAlpha( 0.9f ), 1.5f );\n\t\t\t\tEditor.Paint.DrawRect( box, 1f * _scale );\n\t\t\t}\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EMove/rotate/scale \u2014 a body with four-way translation arrows through it.\u003C/summary\u003E\n\tprivate static void PaintTransform( Vector2 c, Color color )\n\t{\n\t\tStroked( color.WithAlpha( 0.6f ) );\n\t\tOutline( At( c, -4, -4 ), At( c, 4, -4 ), At( c, 4, 4 ), At( c, -4, 4 ) );\n\n\t\tStroked( color, 1.4f );\n\t\tEditor.Paint.DrawLine( At( c, 0, -5.4f ), At( c, 0, 5.4f ) );\n\t\tEditor.Paint.DrawLine( At( c, -5.4f, 0 ), At( c, 5.4f, 0 ) );\n\n\t\tArrowHead( At( c, 0, -8.2f ), new Vector2( 0, -1 ), color, 3f );\n\t\tArrowHead( At( c, 0, 8.2f ), new Vector2( 0, 1 ), color, 3f );\n\t\tArrowHead( At( c, -8.2f, 0 ), new Vector2( -1, 0 ), color, 3f );\n\t\tArrowHead( At( c, 8.2f, 0 ), new Vector2( 1, 0 ), color, 3f );\n\t}\n\n\t/// \u003Csummary\u003EA UV grid with one texel lit, and the projection arriving from off-surface.\u003C/summary\u003E\n\tprivate static void PaintUVProject( Vector2 c, Color color )\n\t{\n\t\tStroked( color );\n\t\tOutline( At( c, -7, -2 ), At( c, 7, -2 ), At( c, 7, 8 ), At( c, -7, 8 ) );\n\n\t\tStroked( color.WithAlpha( 0.6f ), 1.1f );\n\t\tEditor.Paint.DrawLine( At( c, -2.4f, -2 ), At( c, -2.4f, 8 ) );\n\t\tEditor.Paint.DrawLine( At( c, 2.4f, -2 ), At( c, 2.4f, 8 ) );\n\t\tEditor.Paint.DrawLine( At( c, -7, 3 ), At( c, 7, 3 ) );\n\n\t\t// One lit texel, so the grid reads as a texture rather than a wireframe.\n\t\tFilled( color.WithAlpha( 0.8f ) );\n\t\tEditor.Paint.DrawRect( Box( c, -6.3f, -1.3f, 3.8f, 3.6f ), 0.6f * _scale );\n\n\t\t// The projection coming down onto it.\n\t\tStroked( color, 1.4f );\n\t\tEditor.Paint.DrawLine( At( c, 0, -8.4f ), At( c, 0, -4.4f ) );\n\t\tArrowHead( At( c, 0, -2.6f ), new Vector2( 0, 1 ), color, 3f );\n\t}\n\n\t/// \u003Csummary\u003EA cube with ONE of its three visible faces filled \u2014 the operation is \u0022this face,\n\t/// not that one\u0022, so what the glyph has to show is faces being told apart. A paint pot or a\n\t/// swatch would say \u0022material\u0022 without saying \u0022per face\u0022, which is the whole distinction.\u003C/summary\u003E\n\tprivate static void PaintFaceMaterial( Vector2 c, Color color )\n\t{\n\t\t// Isometric cube: top rhombus, then the two visible side quads.\n\t\tvar top = At( c, 0, -8 );\n\t\tvar right = At( c, 8, -3.5f );\n\t\tvar bottom = At( c, 0, 1 );\n\t\tvar left = At( c, -8, -3.5f );\n\n\t\t// The lit face, filled. DrawPolygon fills, which is exactly what is wanted here and is why\n\t\t// the other faces are walked as lines instead.\n\t\tFilled( color.WithAlpha( 0.85f ) );\n\t\tEditor.Paint.DrawPolygon( top, right, bottom, left );\n\n\t\tStroked( color );\n\t\tOutline( top, right, bottom, left );\n\n\t\t// The two side faces, left plain so the filled top reads as the odd one out.\n\t\tvar lowLeft = At( c, -8, 5.5f );\n\t\tvar lowMid = At( c, 0, 10 );\n\t\tvar lowRight = At( c, 8, 5.5f );\n\n\t\tOutline( left, bottom, lowMid, lowLeft );\n\t\tOutline( bottom, right, lowRight, lowMid );\n\t}\n\n\t// --- sketch tools ---------------------------------------------------------------------------\n\t//\n\t// One rule for the whole row: SHOW THE SHAPE THE TOOL MAKES, AND SHOW HOW IT IS PLACED.\n\t//\n\t// The second half is what earns its keep. Every family behind a chevron draws the identical\n\t// shape and differs only in which points you click \u2014 a corner rectangle and a centre rectangle\n\t// are the same rectangle \u2014 so the shape alone cannot tell them apart. The shape is the body of\n\t// the glyph and the click points are accent dots on it, which makes the pair legible side by\n\t// side without either needing a label.\n\t//\n\t// The dots are annotation and must never outweigh the shape. They were half again this size to\n\t// begin with, which looked right on a large preview and swallowed the geometry at the size these\n\t// are actually seen at.\n\n\t/// \u003Csummary\u003EThe colour of a click point. Deliberately the one warm accent in a monochrome row,\n\t/// so \u0022this is where you press\u0022 reads before anything else does.\u003C/summary\u003E\n\tprivate static readonly Color ClickColor = new( 1f, 0.77f, 0.24f, 1f );\n\n\t/// \u003Csummary\u003EAn end that does not join up. Warm rather than red \u2014 this is information, not an\n\t/// error, and a sketch mid-draw is full of them.\u003C/summary\u003E\n\tprivate static readonly Color LooseEndColor = new( 1f, 0.48f, 0.36f, 1f );\n\n\t/// \u003Csummary\u003EA guide line \u2014 a radius, a diagonal, a centre line. Something the tool uses to place\n\t/// the shape rather than part of the shape itself.\u003C/summary\u003E\n\tprivate static Color GuideColor( Color color ) =\u003E color.WithAlpha( 0.35f );\n\n\t/// \u003Csummary\u003EA filled dot in the nominal icon space. DrawRect with a corner radius of half its\n\t/// own size, since Paint has no circle of its own and this is exact.\u003C/summary\u003E\n\tprivate static void Dot( Vector2 center, float radius, Color color )\n\t{\n\t\tFilled( color );\n\t\tEditor.Paint.DrawRect( Box( center, -radius, -radius, radius * 2f, radius * 2f ), radius * _scale );\n\t}\n\n\tprivate static void ClickDot( Vector2 p, float radius = 1.8f ) =\u003E Dot( p, radius, ClickColor );\n\n\t/// \u003Csummary\u003EThe corners of a regular hexagon, for the two polygon tools.\u003C/summary\u003E\n\tprivate static Vector2[] Hexagon( Vector2 c, float radius, float rotationDegrees )\n\t{\n\t\tvar points = new Vector2[6];\n\n\t\tfor ( var i = 0; i \u003C 6; i\u002B\u002B )\n\t\t{\n\t\t\tvar a = (rotationDegrees \u002B i * 60f) * MathF.PI / 180f;\n\t\t\tpoints[i] = At( c, MathF.Cos( a ) * radius, MathF.Sin( a ) * radius );\n\t\t}\n\n\t\treturn points;\n\t}\n\n\t/// \u003Csummary\u003EA cursor with a point caught under it. Select drags sketch POINTS, which is what the\n\t/// dot says and a bare arrow would not.\u003C/summary\u003E\n\tprivate static void PaintSelectTool( Vector2 c, Color color )\n\t{\n\t\tFilled( color );\n\t\tEditor.Paint.DrawPolygon(\n\t\t\tAt( c, -4, -8 ), At( c, -4, 4 ), At( c, -1, 1 ), At( c, 1.5f, 6 ),\n\t\t\tAt( c, 4, 5 ), At( c, 1.5f, 0.2f ), At( c, 5, -0.5f ) );\n\n\t\tClickDot( At( c, 5, 5 ), 2.2f );\n\t}\n\n\tprivate static void PaintLineTool( Vector2 c, Color color )\n\t{\n\t\tStroked( color, 1.8f );\n\t\tEditor.Paint.DrawLine( At( c, -6.5f, 6 ), At( c, 6.5f, -6 ) );\n\n\t\tClickDot( At( c, -6.5f, 6 ) );\n\t\tClickDot( At( c, 6.5f, -6 ) );\n\t}\n\n\t/// \u003Csummary\u003EThe same line, marked at its MIDDLE - with the tick mark that means midpoint in every\n\t/// CAD package there is. One click dot, because the second click is an end and the far one comes\n\t/// for free.\u003C/summary\u003E\n\tprivate static void PaintLineMidpointTool( Vector2 c, Color color )\n\t{\n\t\tStroked( color, 1.8f );\n\t\tEditor.Paint.DrawLine( At( c, -6.5f, 6 ), At( c, 6.5f, -6 ) );\n\n\t\t// Across the line rather than along it, so it reads as a mark ON the line instead of a second\n\t\t// shorter line beside it.\n\t\tStroked( GuideColor( color ), 1f );\n\t\tEditor.Paint.DrawLine( At( c, -2.1f, -2.3f ), At( c, 2.1f, 2.3f ) );\n\n\t\tClickDot( c, 1.9f );\n\t\tClickDot( At( c, 6.5f, -6 ) );\n\t}\n\n\t/// \u003Csummary\u003ETwo opposite corners marked: click one, then the other.\u003C/summary\u003E\n\tprivate static void PaintRectangleTool( Vector2 c, Color color )\n\t{\n\t\tStroked( color );\n\t\tOutline( At( c, -6.5f, -5 ), At( c, 6.5f, -5 ), At( c, 6.5f, 5 ), At( c, -6.5f, 5 ) );\n\n\t\tClickDot( At( c, -6.5f, -5 ) );\n\t\tClickDot( At( c, 6.5f, 5 ) );\n\t}\n\n\t/// \u003Csummary\u003EThe same rectangle, marked at its CENTRE instead \u2014 with the half-diagonal it is\n\t/// dragged out along.\u003C/summary\u003E\n\tprivate static void PaintRectangleCentreTool( Vector2 c, Color color )\n\t{\n\t\tStroked( color );\n\t\tOutline( At( c, -6.5f, -5 ), At( c, 6.5f, -5 ), At( c, 6.5f, 5 ), At( c, -6.5f, 5 ) );\n\n\t\tStroked( GuideColor( color ), 1f );\n\t\tEditor.Paint.DrawLine( c, At( c, 6.5f, 5 ) );\n\n\t\tClickDot( c, 1.9f );\n\t}\n\n\tprivate static void PaintCircleTool( Vector2 c, Color color )\n\t{\n\t\tStroked( color );\n\t\tArc( c, 6.2f, 0, 360, 28 );\n\n\t\tStroked( GuideColor( color ), 1f );\n\t\tEditor.Paint.DrawLine( c, At( c, 6.2f, 0 ) );\n\n\t\tClickDot( c, 1.9f );\n\t}\n\n\t/// \u003Csummary\u003EThe same circle with three points ON the rim and no centre \u2014 which is precisely the\n\t/// difference between the two ways of placing it.\u003C/summary\u003E\n\tprivate static void PaintCircleThreePointTool( Vector2 c, Color color )\n\t{\n\t\tStroked( color );\n\t\tArc( c, 6.2f, 0, 360, 28 );\n\n\t\tforeach ( var degrees in new[] { -90f, 30f, 150f } )\n\t\t{\n\t\t\tvar a = degrees * MathF.PI / 180f;\n\t\t\tClickDot( At( c, MathF.Cos( a ) * 6.2f, MathF.Sin( a ) * 6.2f ) );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// An arc standing on its centre, with both radii drawn.\n\t///\n\t/// It was drawn small and off to one side first, and at the size these are actually used it read\n\t/// as a tick mark rather than a curve. An arc has to span the box to look like an arc.\n\t/// \u003C/summary\u003E\n\tprivate static void PaintArcTool( Vector2 c, Color color )\n\t{\n\t\t// RADIUS 9, NOT 10.5, AND THE HUB SITS AT 4.2 RATHER THAN 5.5. A radius of 10.5 put the\n\t\t// guide rails 21 units apart inside a box this file says is 18, which made this the second\n\t\t// widest glyph on the strip - and nothing about one icon in isolation shows that. Nine\n\t\t// fills the width exactly, and dropping the hub centres the drawing\u0027s own height in the\n\t\t// button instead of hanging it below the middle.\n\t\tvar hub = At( c, 0, 4.2f );\n\n\t\tStroked( color, 1.9f );\n\t\tArc( hub, 9f, 180, 360, 20 );\n\n\t\tStroked( GuideColor( color ), 1f );\n\t\tEditor.Paint.DrawLine( hub, At( c, -9f, 4.2f ) );\n\t\tEditor.Paint.DrawLine( hub, At( c, 9f, 4.2f ) );\n\n\t\tClickDot( hub, 1.9f );\n\t}\n\n\t/// \u003Csummary\u003EThe same arc with no centre at all, marked instead at both ends and the point it\n\t/// passes through.\u003C/summary\u003E\n\tprivate static void PaintArcThreePointTool( Vector2 c, Color color )\n\t{\n\t\t// A SMALLER ARC THAN PaintArcTool\u0027s, ON PURPOSE. This was the widest glyph on the strip by\n\t\t// a distance - 24.6 units against a nominal 18 - and the arc was not what made it so: the\n\t\t// three dots are the whole point of the tool, they sit ON the arc\u0027s ends, and a 1.8 dot at\n\t\t// x = 10.5 reaches 12.3. The dots are part of the drawing, so the arc gives up the room for\n\t\t// them rather than the pair of icons disagreeing about how wide an arc glyph is: both now\n\t\t// fill exactly 18 units, which is the measurement the eye actually compares.\n\t\tvar hub = At( c, 0, 4.2f );\n\n\t\tStroked( color, 1.9f );\n\t\tArc( hub, 7.2f, 180, 360, 20 );\n\n\t\tClickDot( At( c, -7.2f, 4.2f ) );\n\t\tClickDot( At( c, 0, -3f ) );\n\t\tClickDot( At( c, 7.2f, 4.2f ) );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A polygon with its corners ON the circle.\n\t///\n\t/// The circle is drawn brighter than a guide normally would be, because where it sits relative\n\t/// to the polygon IS the whole difference between this and the circumscribed version. Draw it at\n\t/// guide strength and the two glyphs become the same hexagon.\n\t/// \u003C/summary\u003E\n\tprivate static void PaintPolygonTool( Vector2 c, Color color )\n\t{\n\t\tStroked( color.WithAlpha( 0.6f ), 1.2f );\n\t\tArc( c, 7f, 0, 360, 28 );\n\n\t\tStroked( color, 1.7f );\n\t\tOutline( Hexagon( c, 7f, -90f ) );\n\n\t\tClickDot( c, 1.7f );\n\t}\n\n\t/// \u003Csummary\u003EEdges on the circle instead, so it sits visibly inside the polygon \u2014 the apothem is\n\t/// 0.866 of the radius, a gap wide enough to read small.\u003C/summary\u003E\n\tprivate static void PaintPolygonCircumscribedTool( Vector2 c, Color color )\n\t{\n\t\tStroked( color, 1.7f );\n\t\tOutline( Hexagon( c, 7.6f, -90f ) );\n\n\t\tStroked( color.WithAlpha( 0.6f ), 1.2f );\n\t\tArc( c, 6.6f, 0, 360, 28 );\n\n\t\tClickDot( c, 1.7f );\n\t}\n\n\t/// \u003Csummary\u003EA slot, with the centre line you actually click marked at both ends.\u003C/summary\u003E\n\tprivate static void PaintSlotTool( Vector2 c, Color color )\n\t{\n\t\tconst float r = 4.6f;\n\n\t\tStroked( color );\n\t\tEditor.Paint.DrawLine( At( c, -3, -r ), At( c, 3, -r ) );\n\t\tEditor.Paint.DrawLine( At( c, -3, r ), At( c, 3, r ) );\n\t\tArc( At( c, 3, 0 ), r, -90, 90, 12 );\n\t\tArc( At( c, -3, 0 ), r, 90, 270, 12 );\n\n\t\tStroked( GuideColor( color ), 1f );\n\t\tEditor.Paint.DrawLine( At( c, -3, 0 ), At( c, 3, 0 ) );\n\n\t\tClickDot( At( c, -3, 0 ) );\n\t\tClickDot( At( c, 3, 0 ) );\n\t}\n\n\t/// \u003Csummary\u003ECrosshairs around a point. The gap at the centre is what stops it reading as a plus\n\t/// sign.\u003C/summary\u003E\n\tprivate static void PaintPointTool( Vector2 c, Color color )\n\t{\n\t\tStroked( color, 1.4f );\n\t\tEditor.Paint.DrawLine( At( c, -7, 0 ), At( c, -2.5f, 0 ) );\n\t\tEditor.Paint.DrawLine( At( c, 2.5f, 0 ), At( c, 7, 0 ) );\n\t\tEditor.Paint.DrawLine( At( c, 0, -7 ), At( c, 0, -2.5f ) );\n\t\tEditor.Paint.DrawLine( At( c, 0, 2.5f ), At( c, 0, 7 ) );\n\n\t\tClickDot( c, 2.2f );\n\t}\n\n\t/// \u003Csummary\u003EA dashed line: geometry that guides and never becomes part of a profile. Dashed\n\t/// because that is how construction geometry is drawn in the viewport, so the button and the\n\t/// thing it makes look like each other.\u003C/summary\u003E\n\tprivate static void PaintConstructionTool( Vector2 c, Color color )\n\t{\n\t\tStroked( color, 1.7f );\n\n\t\tforeach ( var (from, to) in new[] { (0f, 0.22f), (0.39f, 0.61f), (0.78f, 1f) } )\n\t\t{\n\t\t\tEditor.Paint.DrawLine(\n\t\t\t\tAt( c, -7 \u002B 14 * from, 6 - 12 * from ),\n\t\t\t\tAt( c, -7 \u002B 14 * to, 6 - 12 * to ) );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A shaded region with a gap in its outline, and the two loose ends called out.\n\t///\n\t/// This is exactly what the inspector shows: which regions closed, and where a chain did not.\n\t/// Drawn first as a stub with a dot on it, which at the size these are seen reads as a box with\n\t/// a speck in the corner and says nothing. The fill has to be solid enough to read as shading and\n\t/// the gap has to be a real hole in the outline.\n\t/// \u003C/summary\u003E\n\tprivate static void PaintProfileInspectorTool( Vector2 c, Color color )\n\t{\n\t\tFilled( color.WithAlpha( 0.41f ) );\n\t\tEditor.Paint.DrawPolygon( At( c, -6.5f, -5 ), At( c, 6.5f, -5 ), At( c, 6.5f, 5 ), At( c, -6.5f, 5 ) );\n\n\t\t// Walked as an open polyline rather than an outline, because the gap is the point.\n\t\tStroked( color, 1.7f );\n\t\tEditor.Paint.DrawLine( At( c, 6.5f, -1.6f ), At( c, 6.5f, -5 ) );\n\t\tEditor.Paint.DrawLine( At( c, 6.5f, -5 ), At( c, -6.5f, -5 ) );\n\t\tEditor.Paint.DrawLine( At( c, -6.5f, -5 ), At( c, -6.5f, 5 ) );\n\t\tEditor.Paint.DrawLine( At( c, -6.5f, 5 ), At( c, 6.5f, 5 ) );\n\t\tEditor.Paint.DrawLine( At( c, 6.5f, 5 ), At( c, 6.5f, 1.6f ) );\n\n\t\tDot( At( c, 6.5f, -1.6f ), 1.9f, LooseEndColor );\n\t\tDot( At( c, 6.5f, 1.6f ), 1.9f, LooseEndColor );\n\t}\n\n\t/// \u003Csummary\u003EA plain tick. The one glyph in the row that must not be clever: it ends the mode,\n\t/// and the confirm colour it is painted in already carries the meaning.\u003C/summary\u003E\n\tprivate static void PaintFinishSketchTool( Vector2 c, Color color )\n\t{\n\t\tStroked( color, 2.2f );\n\t\tEditor.Paint.DrawLine( At( c, -6, 0.5f ), At( c, -1.5f, 5 ) );\n\t\tEditor.Paint.DrawLine( At( c, -1.5f, 5 ), At( c, 6.5f, -5 ) );\n\t}\n\n\t// --- sculpt tools ---------------------------------------------------------------------------\n\t//\n\t// EVERY ONE OF THESE IS A SURFACE AND WHAT HAPPENS TO IT. The obvious way to draw six brushes is\n\t// six brush heads with a small badge each, which at 27px is six identical blobs. Drawing the\n\t// EFFECT instead means the row can be read at a glance without learning it: a bump rising, a\n\t// ripple flattening, a peak dragged sideways.\n\t//\n\t// The surface runs across the lower half so every glyph shares a baseline and the row reads as\n\t// one family.\n\n\t/// \u003Csummary\u003EA surface with a bump pushed up out of it, and the brush\u0027s ring resting on the\n\t/// bump. The feature-strip glyph, so it says \u0022sculpting\u0022 rather than any one brush.\u003C/summary\u003E\n\tprivate static void PaintSculpt( Vector2 c, Color color )\n\t{\n\t\tStroked( color, 1.7f );\n\t\tSurfaceWithBump( c, 4.5f );\n\n\t\t// The brush ring, seen at a slight angle so it reads as sitting ON the surface.\n\t\tStroked( color.WithAlpha( 0.75f ), 1.3f );\n\t\tArc( At( c, 0, -3.4f ), 5.6f, 0f, 360f, 20 );\n\t}\n\n\t/// \u003Csummary\u003EA bump, and an arrow pushing outward from it: draw adds material along the normal.\u003C/summary\u003E\n\tprivate static void PaintSculptDraw( Vector2 c, Color color )\n\t{\n\t\tStroked( color, 1.7f );\n\t\tSurfaceWithBump( c, 4f );\n\n\t\tStroked( color, 1.4f );\n\t\tEditor.Paint.DrawLine( At( c, 0, -1.5f ), At( c, 0, -7f ) );\n\t\tArrowHead( At( c, 0, -8f ), new Vector2( 0, -1 ), color );\n\t}\n\n\t/// \u003Csummary\u003EA ripple above, the same surface calmed below. Smooth is the one brush whose whole\n\t/// meaning is the difference between two lines.\u003C/summary\u003E\n\tprivate static void PaintSculptSmooth( Vector2 c, Color color )\n\t{\n\t\t// Rippled.\n\t\tStroked( color.WithAlpha( 0.85f ), 1.5f );\n\t\tvar previous = At( c, -8.5f, -4f );\n\n\t\tfor ( var i = 1; i \u003C= 24; i\u002B\u002B )\n\t\t{\n\t\t\tvar t = i / 24f;\n\t\t\tvar x = -8.5f \u002B 17f * t;\n\t\t\tvar y = -4f \u002B MathF.Sin( t * MathF.PI * 3f ) * 2.6f;\n\t\t\tvar point = At( c, x, y );\n\n\t\t\tEditor.Paint.DrawLine( previous, point );\n\t\t\tprevious = point;\n\t\t}\n\n\t\t// Calmed.\n\t\tStroked( color, 1.8f );\n\t\tEditor.Paint.DrawLine( At( c, -8.5f, 5f ), At( c, 8.5f, 5f ) );\n\n\t\tStroked( color.WithAlpha( 0.6f ), 1.2f );\n\t\tEditor.Paint.DrawLine( At( c, 0, -0.5f ), At( c, 0, 2.2f ) );\n\t\tArrowHead( At( c, 0, 3.4f ), new Vector2( 0, 1 ), color.WithAlpha( 0.6f ), 2.8f );\n\t}\n\n\t/// \u003Csummary\u003EA closed shape with arrows pushing out all round it \u2014 inflate acts everywhere at\n\t/// once, which is what tells it apart from draw.\u003C/summary\u003E\n\tprivate static void PaintSculptInflate( Vector2 c, Color color )\n\t{\n\t\tStroked( color, 1.7f );\n\t\tArc( c, 4.6f, 0f, 360f, 20 );\n\n\t\tfor ( var i = 0; i \u003C 4; i\u002B\u002B )\n\t\t{\n\t\t\tvar radians = (45f \u002B i * 90f) * MathF.PI / 180f;\n\t\t\tvar dir = new Vector2( MathF.Cos( radians ), MathF.Sin( radians ) );\n\n\t\t\tStroked( color.WithAlpha( 0.9f ), 1.3f );\n\t\t\tEditor.Paint.DrawLine( c \u002B dir * 5.8f * _scale, c \u002B dir * 8f * _scale );\n\t\t\tArrowHead( c \u002B dir * 9.2f * _scale, dir, color, 2.8f );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EA surface dragged sideways into a lean, with the pull shown as an arrow. Grab moves\n\t/// what it holds rather than adding to it, so nothing here points along the normal.\u003C/summary\u003E\n\tprivate static void PaintSculptGrab( Vector2 c, Color color )\n\t{\n\t\tStroked( color, 1.7f );\n\n\t\t// A peak that leans right, rather than a symmetric bump.\n\t\tEditor.Paint.DrawLine( At( c, -8.5f, 5f ), At( c, -2.5f, 5f ) );\n\t\tEditor.Paint.DrawLine( At( c, -2.5f, 5f ), At( c, 2.5f, -3f ) );\n\t\tEditor.Paint.DrawLine( At( c, 2.5f, -3f ), At( c, 5.5f, 5f ) );\n\t\tEditor.Paint.DrawLine( At( c, 5.5f, 5f ), At( c, 8.5f, 5f ) );\n\n\t\tStroked( color.WithAlpha( 0.9f ), 1.4f );\n\t\tEditor.Paint.DrawLine( At( c, -2f, -6f ), At( c, 3.5f, -6f ) );\n\t\tArrowHead( At( c, 5f, -6f ), new Vector2( 1, 0 ), color );\n\t}\n\n\t/// \u003Csummary\u003EA bump with a straight edge laid across it \u2014 flatten is a plane meeting a surface.\n\t/// \u003C/summary\u003E\n\tprivate static void PaintSculptFlatten( Vector2 c, Color color )\n\t{\n\t\tStroked( color.WithAlpha( 0.65f ), 1.5f );\n\t\tSurfaceWithBump( c, 5.5f );\n\n\t\t// The plane it is being cut back to.\n\t\tStroked( color, 2f );\n\t\tEditor.Paint.DrawLine( At( c, -8.5f, -2.5f ), At( c, 8.5f, -2.5f ) );\n\t}\n\n\t/// \u003Csummary\u003ETwo arrows squeezing towards one ridge. Pinch gathers a surface rather than moving\n\t/// it, so both arrows point inward at the same line.\u003C/summary\u003E\n\tprivate static void PaintSculptPinch( Vector2 c, Color color )\n\t{\n\t\tStroked( color, 1.8f );\n\t\tEditor.Paint.DrawLine( At( c, 0, -7.5f ), At( c, 0, 7.5f ) );\n\n\t\tStroked( color.WithAlpha( 0.9f ), 1.4f );\n\t\tEditor.Paint.DrawLine( At( c, -8f, 0 ), At( c, -3.5f, 0 ) );\n\t\tArrowHead( At( c, -2.2f, 0 ), new Vector2( 1, 0 ), color );\n\n\t\tEditor.Paint.DrawLine( At( c, 8f, 0 ), At( c, 3.5f, 0 ) );\n\t\tArrowHead( At( c, 2.2f, 0 ), new Vector2( -1, 0 ), color );\n\t}\n\n\t/// \u003Csummary\u003EA patch of the surface hatched off. Masking protects rather than shapes, so this is\n\t/// the one sculpt glyph that is not a deformation.\u003C/summary\u003E\n\tprivate static void PaintSculptMask( Vector2 c, Color color )\n\t{\n\t\tStroked( color.WithAlpha( 0.85f ), 1.5f );\n\t\tOutline( At( c, -8, -6.5f ), At( c, 8, -6.5f ), At( c, 8, 6.5f ), At( c, -8, 6.5f ) );\n\n\t\t// Hatching, the universal \u0022held back\u0022 texture.\n\t\tStroked( color.WithAlpha( 0.7f ), 1.2f );\n\n\t\tfor ( var x = -6f; x \u003C= 8f; x \u002B= 3.4f )\n\t\t{\n\t\t\tvar top = MathF.Max( x - 13f, -8f );\n\t\t\tvar bottom = MathF.Min( x, 8f );\n\n\t\t\tEditor.Paint.DrawLine( At( c, bottom, -6.5f ), At( c, top, 6.5f ) );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EA coarse grid with a chevron down: fewer, bigger faces.\u003C/summary\u003E\n\tprivate static void PaintSculptLevelDown( Vector2 c, Color color ) =\u003E PaintSculptLevel( c, color, 2, down: true );\n\n\t/// \u003Csummary\u003EA fine grid with a chevron up: four times the faces, which is the whole cost.\n\t/// \u003C/summary\u003E\n\tprivate static void PaintSculptLevelUp( Vector2 c, Color color ) =\u003E PaintSculptLevel( c, color, 4, down: false );\n\n\tprivate static void PaintSculptLevel( Vector2 c, Color color, int divisions, bool down )\n\t{\n\t\tconst float Half = 6.5f;\n\n\t\tStroked( color.WithAlpha( 0.9f ), 1.4f );\n\t\tOutline( At( c, -Half, -Half - 1.5f ), At( c, Half, -Half - 1.5f ),\n\t\t\tAt( c, Half, Half - 1.5f ), At( c, -Half, Half - 1.5f ) );\n\n\t\tStroked( color.WithAlpha( 0.65f ), 1f );\n\n\t\tfor ( var i = 1; i \u003C divisions; i\u002B\u002B )\n\t\t{\n\t\t\tvar t = -Half \u002B i * (Half * 2f / divisions);\n\n\t\t\tEditor.Paint.DrawLine( At( c, t, -Half - 1.5f ), At( c, t, Half - 1.5f ) );\n\t\t\tEditor.Paint.DrawLine( At( c, -Half, t - 1.5f ), At( c, Half, t - 1.5f ) );\n\t\t}\n\n\t\t// The chevron, below the grid so the two never overlap at strip size.\n\t\tStroked( color, 1.8f );\n\n\t\tif ( down )\n\t\t{\n\t\t\tEditor.Paint.DrawLine( At( c, -3.5f, 6f ), At( c, 0, 9f ) );\n\t\t\tEditor.Paint.DrawLine( At( c, 0, 9f ), At( c, 3.5f, 6f ) );\n\t\t}\n\t\telse\n\t\t{\n\t\t\tEditor.Paint.DrawLine( At( c, -3.5f, 9f ), At( c, 0, 6f ) );\n\t\t\tEditor.Paint.DrawLine( At( c, 0, 6f ), At( c, 3.5f, 9f ) );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EA dense surface collapsing into a flat square: the sculpt becoming a texture, which\n\t/// is the whole point of the pipeline and the one operation here that produces a file.\u003C/summary\u003E\n\t/// \u003Csummary\u003E\n\t/// A grease pencil laid over a wavy scribble.\n\t///\n\t/// NOT THE SKETCH PENCIL, which this sits two buttons away from and must not be mistaken for.\n\t/// That one is a sharp #2 drawing a straight line and it makes geometry; this is a fat blunt\n\t/// marker over a loose squiggle, and the squiggle is doing the work \u2014 a scribble is what\n\t/// handwriting looks like at 18px, and nothing that produces a solid in this bar is drawn\n\t/// scribbly.\n\t/// \u003C/summary\u003E\n\tprivate static void PaintNoteTool( Vector2 c, Color color )\n\t{\n\t\t// The scribble first, so the marker sits on top of it the way a pen sits on its own line.\n\t\tStroked( color.WithAlpha( 0.75f ), 1.5f );\n\n\t\tvar previous = At( c, -8f, 5.5f );\n\n\t\tfor ( var i = 1; i \u003C= 20; i\u002B\u002B )\n\t\t{\n\t\t\tvar t = i / 20f;\n\t\t\tvar point = At( c, -8f \u002B 13f * t, 5.5f \u002B MathF.Sin( t * MathF.PI * 2.2f ) * 1.8f );\n\n\t\t\tEditor.Paint.DrawLine( previous, point );\n\t\t\tprevious = point;\n\t\t}\n\n\t\t// The barrel, drawn as a slab on the diagonal rather than the sketch pencil\u0027s thin shaft.\n\t\tvar tip = At( c, -5.5f, 1.5f );\n\t\tvar along = (At( c, 7f, -7.5f ) - tip).Normal;\n\t\tvar across = new Vector2( -along.y, along.x ) * (2.6f * _scale);\n\n\t\tStroked( color, 1.5f );\n\t\tOutline(\n\t\t\ttip \u002B across * 0.35f,\n\t\t\tAt( c, 7f, -7.5f ) \u002B across,\n\t\t\tAt( c, 8.5f, -8.5f ) \u002B across,\n\t\t\tAt( c, 8.5f, -8.5f ) - across,\n\t\t\tAt( c, 7f, -7.5f ) - across,\n\t\t\ttip - across * 0.35f );\n\n\t\t// The nib, filled: the one part of a marker that is a different colour from the barrel, and\n\t\t// what makes the shape read as pointing at the scribble rather than away from it.\n\t\tFilled( color );\n\t\tEditor.Paint.DrawPolygon( tip, At( c, -3.2f, -0.4f ) \u002B across * 0.75f, At( c, -3.2f, -0.4f ) - across * 0.75f );\n\t}\n\n\t/// \u003Csummary\u003EAn eraser on the same scribble, taking a bite out of it. The gap in the line is the\n\t/// whole glyph \u2014 an eraser drawn hovering over an intact scribble is just a second block\n\t/// shape.\u003C/summary\u003E\n\tprivate static void PaintNoteEraseTool( Vector2 c, Color color )\n\t{\n\t\t// Left half of the scribble survives; the right half is where the eraser has been.\n\t\tStroked( color.WithAlpha( 0.75f ), 1.5f );\n\n\t\tvar previous = At( c, -8.5f, 5.5f );\n\n\t\tfor ( var i = 1; i \u003C= 10; i\u002B\u002B )\n\t\t{\n\t\t\tvar t = i / 20f;\n\t\t\tvar point = At( c, -8.5f \u002B 13f * t, 5.5f \u002B MathF.Sin( t * MathF.PI * 2.2f ) * 1.8f );\n\n\t\t\tEditor.Paint.DrawLine( previous, point );\n\t\t\tprevious = point;\n\t\t}\n\n\t\t// The block, tilted so it reads as being pushed along the line rather than parked on it.\n\t\tvar down = new Vector2( 0.34f, 0.94f );\n\t\tvar side = new Vector2( -down.y, down.x );\n\t\tvar centre = At( c, 3f, -1f );\n\t\tvar half = 4.6f * _scale;\n\t\tvar length = 5.6f * _scale;\n\n\t\tStroked( color, 1.5f );\n\t\tOutline(\n\t\t\tcentre - down * length \u002B side * half,\n\t\t\tcentre \u002B down * length \u002B side * half,\n\t\t\tcentre \u002B down * length - side * half,\n\t\t\tcentre - down * length - side * half );\n\n\t\t// The ferrule line across it, which is what separates an eraser from a plain rectangle.\n\t\tEditor.Paint.DrawLine( centre \u002B side * half, centre - side * half );\n\t}\n\n\tprivate static void PaintSculptBake( Vector2 c, Color color )\n\t{\n\t\t// The sculpted surface, up top.\n\t\tStroked( color.WithAlpha( 0.85f ), 1.5f );\n\t\tvar previous = At( c, -8.5f, -5f );\n\n\t\tfor ( var i = 1; i \u003C= 20; i\u002B\u002B )\n\t\t{\n\t\t\tvar t = i / 20f;\n\t\t\tvar x = -8.5f \u002B 17f * t;\n\t\t\tvar y = -5f \u002B MathF.Sin( t * MathF.PI * 2f ) * 2.2f;\n\t\t\tvar point = At( c, x, y );\n\n\t\t\tEditor.Paint.DrawLine( previous, point );\n\t\t\tprevious = point;\n\t\t}\n\n\t\t// Into the map.\n\t\tStroked( color.WithAlpha( 0.7f ), 1.2f );\n\t\tEditor.Paint.DrawLine( At( c, 0, -1f ), At( c, 0, 1.6f ) );\n\t\tArrowHead( At( c, 0, 2.8f ), new Vector2( 0, 1 ), color.WithAlpha( 0.7f ), 2.8f );\n\n\t\tStroked( color, 1.6f );\n\t\tOutline( At( c, -7, 4f ), At( c, 7, 4f ), At( c, 7, 9f ), At( c, -7, 9f ) );\n\n\t\tFilled( color.WithAlpha( 0.3f ) );\n\t\tEditor.Paint.DrawRect( Box( c, -7, 4f, 14f, 5f ) );\n\t}\n\n\t/// \u003Csummary\u003EA paint brush: a handle, a ferrule, and a bristle tip dragging a stroke of colour\n\t/// behind it. The stroke is the point \u2014 this is a tool that leaves marks, not one that changes\n\t/// the surface\u0027s shape.\u003C/summary\u003E\n\tprivate static void PaintPaint( Vector2 c, Color color )\n\t{\n\t\t// The stroke it leaves, drawn first so it sits behind the brush.\n\t\tStroked( color.WithAlpha( 0.7f ), 2.4f );\n\t\tEditor.Paint.DrawLine( At( c, 4.5f, 5f ), At( c, -1f, -6f ) );\n\n\t\t// The handle, tilted.\n\t\tStroked( color, 2f );\n\t\tEditor.Paint.DrawLine( At( c, -3f, 8f ), At( c, -1.5f, 3.5f ) );\n\n\t\t// The ferrule.\n\t\tEditor.Paint.DrawLine( At( c, -2f, 3.5f ), At( c, 2.5f, 1.2f ) );\n\n\t\t// The bristle tip.\n\t\tFilled( color );\n\t\tOutline( At( c, 1.6f, 1f ), At( c, 3.4f, -2f ), At( c, 4.5f, -3.6f ), At( c, 6.4f, -1.8f ) );\n\t}\n\n\t/// \u003Csummary\u003EThe same brush as PaintPaint, with the ferrule separated by a hair so the stroke\n\t/// reads as a wide band rather than a line.\u003C/summary\u003E\n\tprivate static void PaintPaintBrush( Vector2 c, Color color )\n\t{\n\t\tStroked( color.WithAlpha( 0.7f ), 3.2f );\n\t\tEditor.Paint.DrawLine( At( c, 5f, 4f ), At( c, -1f, -7f ) );\n\n\t\tStroked( color, 2f );\n\t\tEditor.Paint.DrawLine( At( c, -3f, 8f ), At( c, -1.5f, 3.5f ) );\n\t\tEditor.Paint.DrawLine( At( c, -2f, 3.5f ), At( c, 2.5f, 1.2f ) );\n\n\t\tFilled( color );\n\t\tOutline( At( c, 1.6f, 1f ), At( c, 3.4f, -2f ), At( c, 4.5f, -3.6f ), At( c, 6.4f, -1.8f ) );\n\t}\n\n\t/// \u003Csummary\u003EAn eraser block, worn at one corner, with its band cut away. Drawn as stationery\n\t/// rather than as a mark, for the same reason the notes are: it removes rather than adds.\u003C/summary\u003E\n\tprivate static void PaintPaintEraser( Vector2 c, Color color )\n\t{\n\t\tvar half = 4.5f;\n\t\tvar length = 7f;\n\n\t\tStroked( color.WithAlpha( 0.7f ), 1.8f );\n\t\tEditor.Paint.DrawLine( At( c, -6f, 3.5f ), At( c, 3f, -6f ) );\n\n\t\tStroked( color, 1.8f );\n\t\tvar topLeft = At( c, -half, -length );\n\t\tvar topRight = At( c, half, -length );\n\t\tvar bottomLeft = At( c, -half, length );\n\n\t\t// Worn corner: the bottom-right corner is cut off.\n\t\tOutline( topLeft, topRight, At( c, half, 2f ), At( c, 2f, length ), bottomLeft );\n\n\t\t// The band.\n\t\tEditor.Paint.DrawLine( At( c, -half, -3f ), At( c, half, -3f ) );\n\t}\n\n\n\t/// \u003Csummary\u003EThe shared baseline: a flat surface with one smooth bump in the middle of it. Every\n\t/// brush glyph starts from this so the row reads as one family acting on one thing.\u003C/summary\u003E\n\tprivate static void SurfaceWithBump( Vector2 c, float height )\n\t{\n\t\tvar previous = At( c, -8.5f, 5f );\n\n\t\tfor ( var i = 1; i \u003C= 24; i\u002B\u002B )\n\t\t{\n\t\t\tvar t = i / 24f;\n\t\t\tvar x = -8.5f \u002B 17f * t;\n\n\t\t\t// A raised cosine, flat at both ends so it meets the surface without a corner.\n\t\t\tvar bump = 0.5f * (1f \u002B MathF.Cos( MathF.Max( MathF.Min( x / 5.5f, 1f ), -1f ) * MathF.PI ));\n\t\t\tvar point = At( c, x, 5f - bump * height );\n\n\t\t\tEditor.Paint.DrawLine( previous, point );\n\t\t\tprevious = point;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A wall leaning off vertical, with the vertical it leans from left dashed beside it.\n\t///\n\t/// The angle IS the operation, so the glyph is the angle. Drawing a moulded part instead would\n\t/// say \u0022moulding\u0022 and leave you guessing which of the six tools on the strip does the leaning.\n\t/// \u003C/summary\u003E\n\t/// \u003Csummary\u003E\n\t/// A face lifting off the solid it belongs to: the body drawn faintly where it was, the face\n\t/// itself solid at its new height, and an arrow between them.\n\t///\n\t/// THE GHOST IS THE WHOLE GLYPH. Without it this is an arrow over a rectangle, which is what\n\t/// Transform looks like; with it, the picture is of one face of a part having moved and the rest\n\t/// having stayed \u2014 which is exactly what the tool does and what tells it apart from Draft\n\t/// standing next to it.\n\t/// \u003C/summary\u003E\n\tprivate static void PaintMoveFace( Vector2 c, Color color )\n\t{\n\t\t// Where the face was, and the walls that stretched to follow it.\n\t\tStroked( color.WithAlpha( 0.35f ), 1.1f );\n\t\tOutline( At( c, -7f, 1f ), At( c, 7f, 1f ), At( c, 7f, 8f ), At( c, -7f, 8f ) );\n\n\t\t// The face, at its new height.\n\t\tStroked( color, 1.7f );\n\t\tEditor.Paint.DrawLine( At( c, -7f, -5f ), At( c, 7f, -5f ) );\n\n\t\t// The sides it dragged up with it.\n\t\tStroked( color.WithAlpha( 0.75f ), 1.2f );\n\t\tEditor.Paint.DrawLine( At( c, -7f, -5f ), At( c, -7f, 1f ) );\n\t\tEditor.Paint.DrawLine( At( c, 7f, -5f ), At( c, 7f, 1f ) );\n\n\t\t// Which way it went.\n\t\tStroked( color, 1.6f );\n\t\tEditor.Paint.DrawLine( At( c, 0f, -1f ), At( c, 0f, -8f ) );\n\t\tEditor.Paint.DrawLine( At( c, -3f, -5f ), At( c, 0f, -8f ) );\n\t\tEditor.Paint.DrawLine( At( c, 3f, -5f ), At( c, 0f, -8f ) );\n\t}\n\n\tprivate static void PaintDraft( Vector2 c, Color color )\n\t{\n\t\t// The parting line the taper is measured from.\n\t\tStroked( color.WithAlpha( 0.45f ), 1.1f );\n\t\tfor ( var x = -9f; x \u003C 9f; x \u002B= 3.4f )\n\t\t\tEditor.Paint.DrawLine( At( c, x, 0 ), At( c, x \u002B 2f, 0 ) );\n\n\t\t// The tapered wall: narrow at the top, wide at the bottom, closed as an outline.\n\t\tStroked( color, 1.7f );\n\t\tOutline( At( c, -3.4f, -8 ), At( c, 3.4f, -8 ), At( c, 6.6f, 8 ), At( c, -6.6f, 8 ) );\n\n\t\t// The vertical it is leaning away from, so the lean reads as deliberate rather than as a\n\t\t// wonky rectangle.\n\t\tStroked( color.WithAlpha( 0.5f ), 1f );\n\t\tEditor.Paint.DrawLine( At( c, 3.4f, -8 ), At( c, 3.4f, 8 ) );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A counterbore in section: a wide mouth stepping down to a narrow shaft, through a plate.\n\t///\n\t/// Drawn as a SECTION rather than as a circle on a surface, because a circle is what every other\n\t/// round thing on this strip already looks like from above - and the step is the whole reason\n\t/// this is a feature rather than a sketched circle.\n\t/// \u003C/summary\u003E\n\tprivate static void PaintHole( Vector2 c, Color color )\n\t{\n\t\t// The plate, in section.\n\t\tStroked( color.WithAlpha( 0.8f ), 1.5f );\n\t\tEditor.Paint.DrawLine( At( c, -9, -6 ), At( c, -3.2f, -6 ) );\n\t\tEditor.Paint.DrawLine( At( c, 3.2f, -6 ), At( c, 9, -6 ) );\n\t\tEditor.Paint.DrawLine( At( c, -9, 7 ), At( c, 9, 7 ) );\n\t\tEditor.Paint.DrawLine( At( c, -9, -6 ), At( c, -9, 7 ) );\n\t\tEditor.Paint.DrawLine( At( c, 9, -6 ), At( c, 9, 7 ) );\n\n\t\t// The bore: wide at the mouth, stepping in to the shaft.\n\t\tStroked( color, 1.7f );\n\t\tEditor.Paint.DrawLine( At( c, -3.2f, -6 ), At( c, -3.2f, -1 ) );\n\t\tEditor.Paint.DrawLine( At( c, -3.2f, -1 ), At( c, -1.4f, -1 ) );\n\t\tEditor.Paint.DrawLine( At( c, -1.4f, -1 ), At( c, -1.4f, 7 ) );\n\n\t\tEditor.Paint.DrawLine( At( c, 3.2f, -6 ), At( c, 3.2f, -1 ) );\n\t\tEditor.Paint.DrawLine( At( c, 3.2f, -1 ), At( c, 1.4f, -1 ) );\n\t\tEditor.Paint.DrawLine( At( c, 1.4f, -1 ), At( c, 1.4f, 7 ) );\n\n\t\t// The void itself, so the shape reads as absence rather than as a post.\n\t\tFilled( color.WithAlpha( 0.18f ) );\n\t\tEditor.Paint.DrawPolygon(\n\t\t\tAt( c, -3.2f, -6 ), At( c, 3.2f, -6 ), At( c, 3.2f, -1 ), At( c, 1.4f, -1 ),\n\t\t\tAt( c, 1.4f, 7 ), At( c, -1.4f, 7 ), At( c, -1.4f, -1 ), At( c, -3.2f, -1 ) );\n\t}\n\t// --- the six sketch tools -------------------------------------------------------------------\n\t//\n\t// The four EDIT tools all show the same thing: a curve, and what the tool does to it, with the\n\t// part being removed or added drawn faintly. A trim that showed only the result would be\n\t// indistinguishable from a plain line at 27 pixels.\n\n\t/// \u003Csummary\u003EAn ellipse, with its long axis marked so it is not mistaken for a circle.\u003C/summary\u003E\n\tprivate static void PaintEllipseTool( Vector2 c, Color color )\n\t{\n\t\tStroked( color, 1.7f );\n\n\t\tvar previous = Vector2.Zero;\n\n\t\tfor ( var i = 0; i \u003C= 40; i\u002B\u002B )\n\t\t{\n\t\t\tvar a = i / 40f * MathF.PI * 2f;\n\t\t\tvar point = At( c, MathF.Cos( a ) * 8.6f, MathF.Sin( a ) * 5f );\n\n\t\t\tif ( i \u003E 0 )\n\t\t\t\tEditor.Paint.DrawLine( previous, point );\n\n\t\t\tprevious = point;\n\t\t}\n\n\t\tStroked( color.WithAlpha( 0.5f ), 1.1f );\n\t\tEditor.Paint.DrawLine( At( c, -8.6f, 0 ), At( c, 8.6f, 0 ) );\n\t}\n\n\t/// \u003Csummary\u003EA curve through its control points, which is what a spline IS - the points are the\n\t/// thing you place and the curve is what follows.\u003C/summary\u003E\n\tprivate static void PaintSplineTool( Vector2 c, Color color )\n\t{\n\t\tStroked( color, 1.7f );\n\n\t\tvar previous = Vector2.Zero;\n\n\t\t// A cubic-ish wiggle through the three dots below.\n\t\tfor ( var i = 0; i \u003C= 32; i\u002B\u002B )\n\t\t{\n\t\t\tvar t = i / 32f;\n\t\t\tvar x = -7.5f \u002B 15f * t;\n\t\t\tvar y = MathF.Sin( t * MathF.PI * 1.6f \u002B 0.4f ) * 5.2f - 1f;\n\t\t\tvar point = At( c, x, y );\n\n\t\t\tif ( i \u003E 0 )\n\t\t\t\tEditor.Paint.DrawLine( previous, point );\n\n\t\t\tprevious = point;\n\t\t}\n\n\t\tFilled( color );\n\n\t\t// 15 UNITS OF CURVE, NOT 17. The end dots are centred on the curve\u0027s own ends and are 3\n\t\t// units across, so a curve spanning 17 drew a glyph spanning 20 in a box of 18. Fifteen\n\t\t// plus the two half-dots is exactly 18. The dot positions are recomputed from the same\n\t\t// expression as the curve rather than being written out, so the two cannot drift apart.\n\t\tforeach ( var t in new[] { 0f, 0.5f, 1f } )\n\t\t{\n\t\t\tvar x = -7.5f \u002B 15f * t;\n\t\t\tvar y = MathF.Sin( t * MathF.PI * 1.6f \u002B 0.4f ) * 5.2f - 1f;\n\n\t\t\tEditor.Paint.DrawRect( Box( c, x - 1.5f, y - 1.5f, 3f, 3f ), 1.5f * _scale );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003ETwo crossing lines with the stub past the crossing drawn faintly - the piece that\n\t/// goes. Trim is defined by what it removes, so that is what the glyph shows.\u003C/summary\u003E\n\tprivate static void PaintTrimTool( Vector2 c, Color color )\n\t{\n\t\t// The cutting line.\n\t\tStroked( color.WithAlpha( 0.55f ), 1.3f );\n\t\tEditor.Paint.DrawLine( At( c, 2.5f, -8.5f ), At( c, 2.5f, 8.5f ) );\n\n\t\t// The part that stays.\n\t\tStroked( color, 1.9f );\n\t\tEditor.Paint.DrawLine( At( c, -8.5f, 3f ), At( c, 2.5f, 0f ) );\n\n\t\t// The part that goes, dashed.\n\t\tStroked( color.WithAlpha( 0.35f ), 1.5f );\n\t\tfor ( var t = 0f; t \u003C 1f; t \u002B= 0.28f )\n\t\t{\n\t\t\tvar a = At( c, 2.5f \u002B 6f * t, -1.6f * t );\n\t\t\tvar b = At( c, 2.5f \u002B 6f * (t \u002B 0.16f), -1.6f * (t \u002B 0.16f) );\n\n\t\t\tEditor.Paint.DrawLine( a, b );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EA line reaching a boundary, with the new length drawn faintly and an arrow head -\n\t/// the mirror of Trim, and drawn as its mirror so the pair reads as a pair.\u003C/summary\u003E\n\tprivate static void PaintExtendTool( Vector2 c, Color color )\n\t{\n\t\t// The boundary it reaches to.\n\t\tStroked( color.WithAlpha( 0.55f ), 1.3f );\n\t\tEditor.Paint.DrawLine( At( c, 6.5f, -8.5f ), At( c, 6.5f, 8.5f ) );\n\n\t\t// What is there now.\n\t\tStroked( color, 1.9f );\n\t\tEditor.Paint.DrawLine( At( c, -8.5f, 3f ), At( c, -1f, 1f ) );\n\n\t\t// Where it is going.\n\t\tStroked( color.WithAlpha( 0.4f ), 1.4f );\n\t\tEditor.Paint.DrawLine( At( c, -1f, 1f ), At( c, 5f, -0.6f ) );\n\t\tArrowHead( At( c, 6.4f, -1f ), new Vector2( 1f, -0.26f ), color.WithAlpha( 0.75f ), 3f );\n\t}\n\n\t/// \u003Csummary\u003EA rounded corner with the square one it replaces dashed behind it.\u003C/summary\u003E\n\tprivate static void PaintSketchFilletTool( Vector2 c, Color color )\n\t{\n\t\t// The corner that was.\n\t\tStroked( color.WithAlpha( 0.35f ), 1.3f );\n\t\tEditor.Paint.DrawLine( At( c, -8, -7 ), At( c, 7, -7 ) );\n\t\tEditor.Paint.DrawLine( At( c, 7, -7 ), At( c, 7, 8 ) );\n\n\t\t// The corner that is.\n\t\tStroked( color, 1.9f );\n\t\tEditor.Paint.DrawLine( At( c, -8, -7 ), At( c, 0f, -7 ) );\n\t\tArc( At( c, 0f, 0f ), 7f, -90f, 0f, 12 );\n\t\tEditor.Paint.DrawLine( At( c, 7, 0f ), At( c, 7, 8 ) );\n\t}\n\n\t/// \u003Csummary\u003EA shape and a second one running parallel outside it, which is the whole of what an\n\t/// offset is - the same curve, held away at a distance.\u003C/summary\u003E\n\tprivate static void PaintOffsetTool( Vector2 c, Color color )\n\t{\n\t\t// The original.\n\t\tStroked( color, 1.8f );\n\t\tEditor.Paint.DrawLine( At( c, -6, 6 ), At( c, -6, -2 ) );\n\t\tArc( At( c, -1.5f, -2f ), 4.5f, 180f, 270f, 10 );\n\t\tEditor.Paint.DrawLine( At( c, -1.5f, -6.5f ), At( c, 5, -6.5f ) );\n\n\t\t// Its offset, outside and parallel.\n\t\tStroked( color.WithAlpha( 0.55f ), 1.4f );\n\t\tEditor.Paint.DrawLine( At( c, -9.5f, 6 ), At( c, -9.5f, -2 ) );\n\t\tArc( At( c, -1.5f, -2f ), 8f, 180f, 270f, 12 );\n\t\tEditor.Paint.DrawLine( At( c, -1.5f, -10f ), At( c, 5, -10f ) );\n\t}\n\n\t/// \u003Csummary\u003EAn open L and its reflection across a dashed line you click. Distinct from the\n\t/// solid-feature Mirror glyph, which is a filled triangle: this one is a sketch polyline, and\n\t/// the dot on the axis is what says the line is the thing you pick.\u003C/summary\u003E\n\tprivate static void PaintMirrorTool( Vector2 c, Color color )\n\t{\n\t\t// The axis, dashed, copied from PaintMirror so the two glyphs share a language.\n\t\tStroked( color.WithAlpha( 0.5f ), 1.2f );\n\t\tfor ( var y = -8f; y \u003C 8f; y \u002B= 3.6f )\n\t\t\tEditor.Paint.DrawLine( At( c, 0, y ), At( c, 0, y \u002B 2.1f ) );\n\n\t\t// Source: an open L, solid.\n\t\tStroked( color, 1.8f );\n\t\tEditor.Paint.DrawLine( At( c, -6.5f, 6.5f ), At( c, -6.5f, -6.5f ) );\n\t\tEditor.Paint.DrawLine( At( c, -6.5f, -6.5f ), At( c, -1.4f, -6.5f ) );\n\n\t\t// Reflection, fainter, so it reads as a copy rather than as a second original.\n\t\tStroked( color.WithAlpha( 0.5f ), 1.6f );\n\t\tEditor.Paint.DrawLine( At( c, 6.5f, 6.5f ), At( c, 6.5f, -6.5f ) );\n\t\tEditor.Paint.DrawLine( At( c, 6.5f, -6.5f ), At( c, 1.4f, -6.5f ) );\n\n\t\tClickDot( At( c, 0, 0 ) );\n\t}\n\n\t/// \u003Csummary\u003EThe face\u0027s outline drawn faint, with ONE of its edges taken - solid, and in the\n\t/// green the viewport paints reference geometry, so the button and the thing it acts on are\n\t/// obviously the same thing.\u003C/summary\u003E\n\tprivate static void PaintUseTool( Vector2 c, Color color )\n\t{\n\t\tFaint( color );\n\t\tOutline( At( c, -7, -7 ), At( c, 7, -7 ), At( c, 7, 7 ), At( c, -7, 7 ) );\n\n\t\t// The one edge that has been taken.\n\t\tStroked( ReferenceColor, 2.6f );\n\t\tEditor.Paint.DrawLine( At( c, -7, 7 ), At( c, 7, 7 ) );\n\n\t\tClickDot( At( c, 0, 7 ) );\n\t}\n\n\t/// \u003Csummary\u003EThe same square with every edge taken, which is what the button does in one press.\n\t/// \u003C/summary\u003E\n\tprivate static void PaintUseAllTool( Vector2 c, Color color )\n\t{\n\t\tFaint( color );\n\t\tEditor.Paint.DrawLine( At( c, -7, 0 ), At( c, 7, 0 ) );\n\n\t\tStroked( ReferenceColor, 2.6f );\n\t\tOutline( At( c, -7, -7 ), At( c, 7, -7 ), At( c, 7, 7 ), At( c, -7, 7 ) );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A freehand stroke swept across three lines, with the pieces it went through dashed away.\n\t///\n\t/// The STROKE is the subject and is drawn in the red the viewport draws it in, because that is\n\t/// the thing the button is offering to let you do. Trim\u0027s glyph shows one clean crossing; this\n\t/// one shows a wobble through several, which is the difference between the two tools.\n\t/// \u003C/summary\u003E\n\tprivate static void PaintCutTool( Vector2 c, Color color )\n\t{\n\t\t// Three uprights, each with the piece the stroke went through faded out. The gap is what the\n\t\t// tool does; the two solid ends are what it leaves.\n\t\tfor ( var i = 0; i \u003C 3; i\u002B\u002B )\n\t\t{\n\t\t\tvar x = -6f \u002B i * 6f;\n\n\t\t\tStroked( color, 1.8f );\n\t\t\tEditor.Paint.DrawLine( At( c, x, -8.5f ), At( c, x, -2.5f ) );\n\t\t\tEditor.Paint.DrawLine( At( c, x, 3.5f ), At( c, x, 8.5f ) );\n\n\t\t\tStroked( color.WithAlpha( 0.28f ), 1.4f );\n\t\t\tEditor.Paint.DrawLine( At( c, x, -2.5f ), At( c, x, 3.5f ) );\n\t\t}\n\n\t\t// The stroke itself, sagging so that it passes through all three gaps rather than running\n\t\t// straight - straight would be Trim\u0027s glyph with two more lines in it.\n\t\tStroked( CutColor, 2.1f );\n\n\t\tvar previous = At( c, -9f, -2f );\n\n\t\tfor ( var i = 1; i \u003C= 12; i\u002B\u002B )\n\t\t{\n\t\t\tvar t = i / 12f;\n\t\t\tvar point = At( c, -9f \u002B 18f * t, -2f \u002B MathF.Sin( t * MathF.PI ) * 3.5f );\n\n\t\t\tEditor.Paint.DrawLine( previous, point );\n\t\t\tprevious = point;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EThe part of a Use glyph that is still only scenery.\u003C/summary\u003E\n\tprivate static void Faint( Color color ) =\u003E Stroked( color.WithAlpha( 0.4f ), 1.3f );\n\n\t/// \u003Csummary\u003EThe green the sketcher paints a face\u0027s outline in - see SketchReferenceColor in\n\t/// EffigyViewport.Sketching.cs. Kept in step by eye rather than shared, because that one carries\n\t/// an alpha for drawing in the world and this one has to read on a small dark button.\u003C/summary\u003E\n\tprivate static readonly Color ReferenceColor = new( 0.45f, 1f, 0.6f, 1f );\n\n\t/// \u003Csummary\u003EThe red a cut stroke is drawn in - see SketchCutColor in\n\t/// EffigyViewport.SketchTools.cs. Kept in step by eye, the same as ReferenceColor above: that\n\t/// one carries an alpha for drawing in the world and this one has to read on a small dark\n\t/// button.\u003C/summary\u003E\n\tprivate static readonly Color CutColor = new( 1f, 0.45f, 0.35f, 1f );\n\n\t// --- lighting ------------------------------------------------------------------------------\n\t//\n\t// THE THREE RIGS ARE THE SAME SPHERE, LIT THREE WAYS. Three-point, rim and top-down differ only\n\t// in where the light comes from, so drawing them as three different objects would invent a\n\t// distinction that is not there. Each is a circle with the lit part filled and the rest left as\n\t// outline, which is the difference itself rather than a symbol standing in for it.\n\n\t/// \u003Csummary\u003EThe lit sphere the rig glyphs share: an outlined circle with a filled crescent on\n\t/// whichever side the light is on. \u003Cparamref name=\u0022from\u0022/\u003E is the direction the light arrives\n\t/// from, in icon space.\u003C/summary\u003E\n\tprivate static void LitSphere( Vector2 c, Color color, Vector2 from, float radius = 5.2f )\n\t{\n\t\tvar d = from.Normal;\n\n\t\t// The terminator is perpendicular to the light, so the lit cap runs 180 degrees centred on\n\t\t// the light\u0027s own bearing. Drawn as a filled polygon fan rather than an arc, because an\n\t\t// outline would read as a second circle rather than as brightness.\n\t\tvar bearing = MathF.Atan2( d.y, d.x ) * 180f / MathF.PI;\n\t\tvar points = new List\u003CVector2\u003E();\n\n\t\tfor ( var i = 0; i \u003C= 18; i\u002B\u002B )\n\t\t{\n\t\t\tvar t = (bearing - 90f) \u002B 180f * (i / 18f);\n\t\t\tvar radians = t * MathF.PI / 180f;\n\t\t\tpoints.Add( At( c, MathF.Cos( radians ) * radius, MathF.Sin( radians ) * radius ) );\n\t\t}\n\n\t\tFilled( color.WithAlpha( 0.85f ) );\n\t\tEditor.Paint.DrawPolygon( points.ToArray() );\n\n\t\tStroked( color, 1.4f );\n\t\tArc( c, radius, 0f, 360f, 28 );\n\t}\n\n\t/// \u003Csummary\u003EA short ray coming in toward the sphere, so the glyph says which side the lamp is\n\t/// on as well as which side is bright.\u003C/summary\u003E\n\tprivate static void LightRay( Vector2 c, Color color, Vector2 from, float outer = 8.6f, float inner = 6.4f )\n\t{\n\t\tvar d = from.Normal;\n\n\t\tStroked( color.WithAlpha( 0.9f ), 1.3f );\n\t\tEditor.Paint.DrawLine( At( c, d.x * outer, d.y * outer ), At( c, d.x * inner, d.y * inner ) );\n\t}\n\n\t/// \u003Csummary\u003EEven light from every side: a circle with rays all the way round, none of them\n\t/// longer than the others. Full bright has no key direction and the glyph must not imply\n\t/// one \u2014 that is the entire difference between this and the sun below.\u003C/summary\u003E\n\tprivate static void PaintLightFullBright( Vector2 c, Color color )\n\t{\n\t\tFilled( color.WithAlpha( 0.9f ) );\n\t\tEditor.Paint.DrawCircle( c, 4.1f * _scale );\n\n\t\tStroked( color, 1.3f );\n\n\t\tfor ( var i = 0; i \u003C 8; i\u002B\u002B )\n\t\t{\n\t\t\tvar radians = i * 45f * MathF.PI / 180f;\n\t\t\tvar d = new Vector2( MathF.Cos( radians ), MathF.Sin( radians ) );\n\n\t\t\tEditor.Paint.DrawLine( At( c, d.x * 6f, d.y * 6f ), At( c, d.x * 8.4f, d.y * 8.4f ) );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EA bulb: glass, a screw base, and three rays. The one lamp with no direction, drawn\n\t/// as the object rather than as its effect.\u003C/summary\u003E\n\tprivate static void PaintLightPoint( Vector2 c, Color color )\n\t{\n\t\tStroked( color, 1.5f );\n\t\tArc( c, 4.2f, 0f, 360f, 24 );\n\n\t\t// The base, under the glass.\n\t\tEditor.Paint.DrawLine( At( c, -2.2f, 5.2f ), At( c, 2.2f, 5.2f ) );\n\t\tEditor.Paint.DrawLine( At( c, -1.6f, 7f ), At( c, 1.6f, 7f ) );\n\t\tEditor.Paint.DrawLine( At( c, -2.2f, 5.2f ), At( c, -1.6f, 7f ) );\n\t\tEditor.Paint.DrawLine( At( c, 2.2f, 5.2f ), At( c, 1.6f, 7f ) );\n\n\t\tStroked( color.WithAlpha( 0.8f ), 1.2f );\n\t\tEditor.Paint.DrawLine( At( c, -7.4f, -4.6f ), At( c, -5.6f, -3.4f ) );\n\t\tEditor.Paint.DrawLine( At( c, 0f, -8.2f ), At( c, 0f, -6.2f ) );\n\t\tEditor.Paint.DrawLine( At( c, 7.4f, -4.6f ), At( c, 5.6f, -3.4f ) );\n\t}\n\n\t/// \u003Csummary\u003EA cone of light widening downward, with the pool it lands in. Aimed, which is the\n\t/// property that separates it from the bulb.\u003C/summary\u003E\n\tprivate static void PaintLightSpot( Vector2 c, Color color )\n\t{\n\t\t// The housing.\n\t\tStroked( color, 1.5f );\n\t\tOutline(\n\t\t\tAt( c, -3f, -7.6f ),\n\t\t\tAt( c, 3f, -7.6f ),\n\t\t\tAt( c, 2.2f, -4.4f ),\n\t\t\tAt( c, -2.2f, -4.4f ) );\n\n\t\t// The cone, dimmer than the lamp that casts it.\n\t\tStroked( color.WithAlpha( 0.75f ), 1.3f );\n\t\tEditor.Paint.DrawLine( At( c, -2.2f, -4.4f ), At( c, -6.4f, 5.2f ) );\n\t\tEditor.Paint.DrawLine( At( c, 2.2f, -4.4f ), At( c, 6.4f, 5.2f ) );\n\n\t\t// The pool on the floor, flattened so it reads as ground rather than as a ball.\n\t\tEllipseArc( At( c, 0f, 5.2f ), 6.4f, 1.9f, 0f, 360f, 26 );\n\t}\n\n\t/// \u003Csummary\u003EParallel rays at an angle: a directional light has no position, only a bearing, and\n\t/// parallel is the one thing that says \u0022these rays never converge\u0022.\u003C/summary\u003E\n\tprivate static void PaintLightSun( Vector2 c, Color color )\n\t{\n\t\tFilled( color.WithAlpha( 0.9f ) );\n\t\tEditor.Paint.DrawCircle( At( c, -3.4f, -3.4f ), 3.2f * _scale );\n\n\t\tStroked( color.WithAlpha( 0.85f ), 1.3f );\n\n\t\t// Three rays on the same bearing, spaced across the diagonal. Arrow heads because a sun is\n\t\t// the only lamp here whose glyph would otherwise be three plain lines.\n\t\tfor ( var i = -1; i \u003C= 1; i\u002B\u002B )\n\t\t{\n\t\t\tvar offset = new Vector2( i * 4.6f, -i * 4.6f );\n\t\t\tvar tail = At( c, 1.4f \u002B offset.x, 1.4f \u002B offset.y );\n\t\t\tvar tip = At( c, 6.6f \u002B offset.x, 6.6f \u002B offset.y );\n\n\t\t\tEditor.Paint.DrawLine( tail, tip );\n\t\t\tArrowHead( tip, new Vector2( 1f, 1f ), color.WithAlpha( 0.85f ), 2.8f );\n\t\t\tStroked( color.WithAlpha( 0.85f ), 1.3f );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EKey, fill and rim: the sphere lit from the upper right, with the two weaker lamps\n\t/// marked as rays on the sides they come from.\u003C/summary\u003E\n\tprivate static void PaintLightRigThreePoint( Vector2 c, Color color )\n\t{\n\t\tLitSphere( c, color, new Vector2( 1f, -1f ) );\n\n\t\tLightRay( c, color, new Vector2( 1f, -1f ) );\n\t\tLightRay( c, color.WithAlpha( 0.5f ), new Vector2( -1f, -0.2f ) );\n\t\tLightRay( c, color.WithAlpha( 0.65f ), new Vector2( -0.3f, 1f ) );\n\t}\n\n\t/// \u003Csummary\u003ELit from behind: the far edge glows and the face is dark. The one rig whose whole\n\t/// point is the outline rather than the surface.\u003C/summary\u003E\n\tprivate static void PaintLightRigRim( Vector2 c, Color color )\n\t{\n\t\tStroked( color.WithAlpha( 0.55f ), 1.4f );\n\t\tArc( c, 5.2f, 0f, 360f, 28 );\n\n\t\t// Only the far crescent, drawn heavy. No fill at all, because a rim light leaves the body\n\t\t// of the shape unlit and filling it would be the opposite of what this means.\n\t\tStroked( color, 2.4f );\n\t\tArc( c, 5.2f, -155f, -25f, 20 );\n\n\t\tLightRay( c, color, new Vector2( 0.2f, -1f ) );\n\t}\n\n\t/// \u003Csummary\u003ELit from straight above: the lamp is drawn, and the top of the sphere is bright.\u003C/summary\u003E\n\tprivate static void PaintLightRigTop( Vector2 c, Color color )\n\t{\n\t\tStroked( color, 1.4f );\n\t\tEditor.Paint.DrawLine( At( c, -4.2f, -8f ), At( c, 4.2f, -8f ) );\n\n\t\tStroked( color.WithAlpha( 0.8f ), 1.2f );\n\t\tEditor.Paint.DrawLine( At( c, -2.6f, -7.2f ), At( c, -3.6f, -5f ) );\n\t\tEditor.Paint.DrawLine( At( c, 0f, -7.2f ), At( c, 0f, -5f ) );\n\t\tEditor.Paint.DrawLine( At( c, 2.6f, -7.2f ), At( c, 3.6f, -5f ) );\n\n\t\tLitSphere( At( c, 0f, 1.6f ), color, new Vector2( 0f, -1f ), 4.6f );\n\t}\n\n\t/// \u003Csummary\u003EOne lamp and nothing else: the sphere lit hard from one side, the other side left\n\t/// empty. No fill ray anywhere, which is the difference from three-point.\u003C/summary\u003E\n\tprivate static void PaintLightRigKey( Vector2 c, Color color )\n\t{\n\t\tLitSphere( c, color, new Vector2( 1f, -1f ) );\n\t\tLightRay( c, color, new Vector2( 1f, -1f ) );\n\t}\n\n\t/// \u003Csummary\u003EA bulb with a stroke through it. Clear is the only button on the stage that takes\n\t/// light away, and a struck-through glyph is the one shape that reads as removal without\n\t/// needing a colour to say so.\u003C/summary\u003E\n\tprivate static void PaintLightClear( Vector2 c, Color color )\n\t{\n\t\tvar dim = color.WithAlpha( 0.55f );\n\n\t\tStroked( dim, 1.5f );\n\t\tArc( At( c, 0f, -1f ), 4.2f, 0f, 360f, 24 );\n\n\t\tEditor.Paint.DrawLine( At( c, -2.2f, 4.2f ), At( c, 2.2f, 4.2f ) );\n\t\tEditor.Paint.DrawLine( At( c, -1.6f, 6f ), At( c, 1.6f, 6f ) );\n\t\tEditor.Paint.DrawLine( At( c, -2.2f, 4.2f ), At( c, -1.6f, 6f ) );\n\t\tEditor.Paint.DrawLine( At( c, 2.2f, 4.2f ), At( c, 1.6f, 6f ) );\n\n\t\tStroked( color, 1.9f );\n\t\tEditor.Paint.DrawLine( At( c, -7f, 7f ), At( c, 7f, -7f ) );\n\t}\n\n\t// --- rig ----------------------------------------------------------------------------------\n\n\t/// \u003Csummary\u003E\n\t/// The dog bone the viewport already draws, flattened to two dimensions.\n\t///\n\t/// SAME SHAPE THE TOOL COMMITS, for the same reason EffigyViewport.Rig.cs previews with the\n\t/// real DrawDogBone rather than a placeholder dot: the button and the thing it makes should be\n\t/// recognisably one object. A knob at each end and a tapered shaft between them is the whole\n\t/// silhouette - it is Blender\u0027s octahedral bone seen side-on, which is the form anyone arriving\n\t/// from a rigging tool already reads as \u0022bone\u0022 without being told.\n\t/// \u003C/summary\u003E\n\tprivate static void PaintBone( Vector2 c, Color color )\n\t{\n\t\t// Head at the top, tail at the bottom - bones hang downward in every tree view this\n\t\t// editor draws, so a vertical glyph matches the direction the rig panel lists them in.\n\t\tvar head = At( c, 0f, -7f );\n\t\tvar tail = At( c, 0f, 7f );\n\n\t\t// The shaft, widest a third of the way down from the head. That is where DogBone puts its\n\t\t// cross-section, and a diamond with its waist at the midpoint reads as a kite instead.\n\t\tFilled( color.WithAlpha( 0.28f ) );\n\t\tEditor.Paint.DrawPolygon( head, At( c, -3.4f, -2.4f ), tail, At( c, 3.4f, -2.4f ) );\n\n\t\tStroked( color );\n\t\tOutline( head, At( c, -3.4f, -2.4f ), tail, At( c, 3.4f, -2.4f ) );\n\n\t\t// The joints. Solid, because they are the part you click and drag.\n\t\tFilled( color );\n\t\tEditor.Paint.DrawCircle( head, 2.6f * _scale );\n\t\tEditor.Paint.DrawCircle( tail, 2f * _scale );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A bone with a body pinned to it - the bone at the left, a solid at the right, a tie between.\n\t///\n\t/// The bone half is deliberately the SAME silhouette as PaintBone rather than a fresh drawing,\n\t/// only smaller and turned: Add Bone and Assign Body sit on adjacent stages of the rig bar, and\n\t/// two unrelated glyphs would hide that they are about the same object.\n\t/// \u003C/summary\u003E\n\tprivate static void PaintBoneBind( Vector2 c, Color color )\n\t{\n\t\tvar head = At( c, -8f, 3f );\n\t\tvar tail = At( c, -1.5f, -3f );\n\n\t\tStroked( color );\n\t\tOutline( head, At( c, -6.6f, -1.6f ), tail, At( c, -2.9f, 1.6f ) );\n\n\t\tFilled( color );\n\t\tEditor.Paint.DrawCircle( head, 2f * _scale );\n\t\tEditor.Paint.DrawCircle( tail, 1.5f * _scale );\n\n\t\t// The body it is being pinned to: outlined, the same way PaintMirror outlines the copy, so\n\t\t// the bone stays the solid thing and the body stays the thing being attached to it.\n\t\tStroked( color, 1.4f );\n\t\tEditor.Paint.DrawRect( Box( c, 2f, -1f, 7f, 7f ), 1f );\n\n\t\t// The tie. Dashed, because the assignment is a named link rather than geometry.\n\t\tStroked( color.WithAlpha( 0.6f ), 1.2f );\n\t\tfor ( var x = -1.2f; x \u003C 2f; x \u002B= 1.6f )\n\t\t\tEditor.Paint.DrawLine( At( c, x, -1.6f ), At( c, x \u002B 0.9f, -1.6f ) );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A bone that has swung off its pose, with the pose it left behind still showing.\n\t///\n\t/// THE GHOST IS THE WHOLE ICON. A bone drawn bent means nothing on its own - bones are bent.\n\t/// What says \u0022soft\u0022 is the pair: where the animation put it, dashed, and where the spring\n\t/// actually took it, solid, with the gap between them being exactly the lag the four numbers in\n\t/// the inspector control. Same trick PaintMirror uses, where the outlined copy is only\n\t/// meaningful next to the solid original.\n\t/// \u003C/summary\u003E\n\tprivate static void PaintBoneSoft( Vector2 c, Color color )\n\t{\n\t\tvar head = At( c, -2f, -7f );\n\n\t\t// The pose: where the bone would be if it were rigid. Dashed and faint, because it is the\n\t\t// thing that is NOT happening.\n\t\tStroked( color.WithAlpha( 0.4f ), 1.2f );\n\t\tfor ( var t = 0f; t \u003C 1f; t \u002B= 0.28f )\n\t\t{\n\t\t\tvar a = At( c, -2f, -7f \u002B 14f * t );\n\t\t\tvar b = At( c, -2f, -7f \u002B 14f * (t \u002B 0.16f) );\n\t\t\tEditor.Paint.DrawLine( a, b );\n\t\t}\n\n\t\t// The bone as the spring left it: swung out and trailing behind. Same knob-shaft-knob\n\t\t// silhouette as PaintBone so the two read as the same object.\n\t\tvar tail = At( c, 6f, 5.5f );\n\n\t\tStroked( color );\n\t\tOutline( head, At( c, -1f, -1f ), tail, At( c, 3.6f, -2.6f ) );\n\n\t\tFilled( color );\n\t\tEditor.Paint.DrawCircle( head, 2.4f * _scale );\n\t\tEditor.Paint.DrawCircle( tail, 1.9f * _scale );\n\t}\n\n\t/// \u003Csummary\u003EA play triangle with the swing drawn behind it \u2014 the ordinary \u0022run this\u0022 shape,\n\t/// made specific to what it runs so it is not mistaken for a generic play button on a bar that\n\t/// has no other one.\u003C/summary\u003E\n\tprivate static void PaintSoftPreview( Vector2 c, Color color )\n\t{\n\t\t// Motion trails, fading backwards, the way the bone actually arrives at where it is going.\n\t\tStroked( color.WithAlpha( 0.30f ), 1.3f );\n\t\tArc( At( c, -9f, 0f ), 7.5f, -52f, 52f, 10 );\n\n\t\tStroked( color.WithAlpha( 0.55f ), 1.3f );\n\t\tArc( At( c, -9f, 0f ), 11f, -40f, 40f, 10 );\n\n\t\tFilled( color );\n\t\tEditor.Paint.DrawPolygon( At( c, 1.5f, -6.5f ), At( c, 8.5f, 0f ), At( c, 1.5f, 6.5f ) );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A bone dropped back onto its line, with the swing it gave up drawn as a fading arc.\n\t///\n\t/// NOT A REFRESH ARROW, which is the obvious choice and the wrong one: a circular arrow means\n\t/// \u0022do it again\u0022 everywhere else in an editor, and this does the opposite - it stops the thing\n\t/// that is happening and puts it back where it started.\n\t/// \u003C/summary\u003E\n\tprivate static void PaintSoftRest( Vector2 c, Color color )\n\t{\n\t\t// What is being given up: the arc the tail was swinging through, fading out as it goes.\n\t\tStroked( color.WithAlpha( 0.28f ), 1.2f );\n\t\tArc( At( c, 0f, -7f ), 13f, 62f, 90f, 8 );\n\n\t\t// The bone, back on its pose - straight down, the direction PaintBone draws it.\n\t\tvar head = At( c, 0f, -7f );\n\t\tvar tail = At( c, 0f, 6f );\n\n\t\tStroked( color );\n\t\tOutline( head, At( c, -3f, -2.2f ), tail, At( c, 3f, -2.2f ) );\n\n\t\tFilled( color );\n\t\tEditor.Paint.DrawCircle( head, 2.4f * _scale );\n\t\tEditor.Paint.DrawCircle( tail, 1.8f * _scale );\n\n\t\t// The line it came to rest on, so \u0022rest\u0022 is a place rather than only a state.\n\t\tStroked( color.WithAlpha( 0.5f ), 1.4f );\n\t\tEditor.Paint.DrawLine( At( c, -6f, 8.6f ), At( c, 6f, 8.6f ) );\n\t}\n\n\t// --- Model \u003E Edit ---------------------------------------------------------------------------\n\n\t/// \u003Csummary\u003EA cube\u0027s cage with one corner grabbed and pulled \u2014 editing the mesh itself.\u003C/summary\u003E\n\tprivate static void PaintMeshEdit( Vector2 c, Color color )\n\t{\n\t\tStroked( color.WithAlpha( 0.55f ), 1.2f );\n\t\tOutline( At( c, -7, -3 ), At( c, 3, -3 ), At( c, 3, 7 ), At( c, -7, 7 ) );\n\t\tEditor.Paint.DrawLine( At( c, -7, -3 ), At( c, -3, -7 ) );\n\t\tEditor.Paint.DrawLine( At( c, -3, -7 ), At( c, 7, -8 ) );\n\t\tEditor.Paint.DrawLine( At( c, 7, -8 ), At( c, 3, -3 ) );\n\t\tEditor.Paint.DrawLine( At( c, 7, -8 ), At( c, 7, 3 ) );\n\t\tEditor.Paint.DrawLine( At( c, 7, 3 ), At( c, 3, 7 ) );\n\t\tDot( At( c, 7, -8 ), 2.4f, color );\n\t}\n\n\t/// \u003Csummary\u003EA 2x2 quad cage. lit: 1 a vertex, 2 an edge, 3 a face \u2014 the pick mode, shown as the\n\t/// part of the same cage it picks.\u003C/summary\u003E\n\tprivate static void PaintCage( Vector2 c, Color color, int lit )\n\t{\n\t\tif ( lit == 3 )\n\t\t{\n\t\t\tFilled( color.WithAlpha( 0.55f ) );\n\t\t\tEditor.Paint.DrawPolygon( At( c, 0, -8 ), At( c, 8, -8 ), At( c, 8, 0 ), At( c, 0, 0 ) );\n\t\t}\n\n\t\tStroked( color.WithAlpha( 0.45f ), 1.1f );\n\t\tOutline( At( c, -8, -8 ), At( c, 8, -8 ), At( c, 8, 8 ), At( c, -8, 8 ) );\n\t\tEditor.Paint.DrawLine( At( c, 0, -8 ), At( c, 0, 8 ) );\n\t\tEditor.Paint.DrawLine( At( c, -8, 0 ), At( c, 8, 0 ) );\n\n\t\tif ( lit == 2 )\n\t\t{\n\t\t\tStroked( color, 2.6f );\n\t\t\tEditor.Paint.DrawLine( At( c, -8, 0 ), At( c, 0, 0 ) );\n\t\t}\n\n\t\tif ( lit == 1 )\n\t\t\tDot( At( c, 0, 0 ), 3f, color );\n\t}\n\n\t/// \u003Csummary\u003EA face with a smaller face inside it and the ring between them.\u003C/summary\u003E\n\tprivate static void PaintInset( Vector2 c, Color color )\n\t{\n\t\tStroked( color.WithAlpha( 0.6f ), 1.3f );\n\t\tOutline( At( c, -8, -8 ), At( c, 8, -8 ), At( c, 8, 8 ), At( c, -8, 8 ) );\n\t\tEditor.Paint.DrawLine( At( c, -8, -8 ), At( c, -4, -4 ) );\n\t\tEditor.Paint.DrawLine( At( c, 8, -8 ), At( c, 4, -4 ) );\n\t\tEditor.Paint.DrawLine( At( c, 8, 8 ), At( c, 4, 4 ) );\n\t\tEditor.Paint.DrawLine( At( c, -8, 8 ), At( c, -4, 4 ) );\n\t\tFilled( color.WithAlpha( 0.8f ) );\n\t\tEditor.Paint.DrawPolygon( At( c, -4, -4 ), At( c, 4, -4 ), At( c, 4, 4 ), At( c, -4, 4 ) );\n\t}\n\n\t/// \u003Csummary\u003EA strip of quads with a new loop running across it, dashed where it is being added.\u003C/summary\u003E\n\tprivate static void PaintLoopCut( Vector2 c, Color color )\n\t{\n\t\tStroked( color.WithAlpha( 0.6f ), 1.3f );\n\t\tOutline( At( c, -8, -6 ), At( c, 8, -6 ), At( c, 8, 6 ), At( c, -8, 6 ) );\n\t\tStroked( color, 2.2f );\n\t\tfor ( var y = -6f; y \u003C 6f; y \u002B= 3f )\n\t\t\tEditor.Paint.DrawLine( At( c, 0, y ), At( c, 0, y \u002B 1.8f ) );\n\t}\n\n\t/// \u003Csummary\u003EThree vertices pulled together into one.\u003C/summary\u003E\n\tprivate static void PaintMerge( Vector2 c, Color color )\n\t{\n\t\tStroked( color.WithAlpha( 0.6f ), 1.3f );\n\t\tEditor.Paint.DrawLine( At( c, -7, -6 ), At( c, 0, 3 ) );\n\t\tEditor.Paint.DrawLine( At( c, 7, -6 ), At( c, 0, 3 ) );\n\t\tEditor.Paint.DrawLine( At( c, 0, -8 ), At( c, 0, 3 ) );\n\t\tDot( At( c, -7, -6 ), 1.6f, color.WithAlpha( 0.6f ) );\n\t\tDot( At( c, 7, -6 ), 1.6f, color.WithAlpha( 0.6f ) );\n\t\tDot( At( c, 0, -8 ), 1.6f, color.WithAlpha( 0.6f ) );\n\t\tDot( At( c, 0, 4 ), 3f, color );\n\t}\n\n\t/// \u003Csummary\u003ETwo faces whose shared edge is fading out \u2014 they become one.\u003C/summary\u003E\n\tprivate static void PaintDissolve( Vector2 c, Color color )\n\t{\n\t\tFilled( color.WithAlpha( 0.3f ) );\n\t\tEditor.Paint.DrawPolygon( At( c, -8, -6 ), At( c, 8, -6 ), At( c, 8, 6 ), At( c, -8, 6 ) );\n\t\tStroked( color, 1.5f );\n\t\tOutline( At( c, -8, -6 ), At( c, 8, -6 ), At( c, 8, 6 ), At( c, -8, 6 ) );\n\t\tStroked( color.WithAlpha( 0.35f ), 1.2f );\n\t\tfor ( var y = -6f; y \u003C 6f; y \u002B= 3f )\n\t\t\tEditor.Paint.DrawLine( At( c, 0, y ), At( c, 0, y \u002B 1f ) );\n\t}\n\n\t/// \u003Csummary\u003EA cage with one face knocked out, leaving a hole.\u003C/summary\u003E\n\tprivate static void PaintDeleteGeometry( Vector2 c, Color color )\n\t{\n\t\tFilled( color.WithAlpha( 0.45f ) );\n\t\tEditor.Paint.DrawPolygon( At( c, -8, -8 ), At( c, 0, -8 ), At( c, 0, 8 ), At( c, -8, 8 ) );\n\t\tEditor.Paint.DrawPolygon( At( c, 0, 0 ), At( c, 8, 0 ), At( c, 8, 8 ), At( c, 0, 8 ) );\n\t\tStroked( color, 1.4f );\n\t\tOutline( At( c, -8, -8 ), At( c, 8, -8 ), At( c, 8, 8 ), At( c, -8, 8 ) );\n\t\tStroked( new Color( 1f, 0.45f, 0.4f ), 1.8f );\n\t\tEditor.Paint.DrawLine( At( c, 2, -6 ), At( c, 6, -2 ) );\n\t\tEditor.Paint.DrawLine( At( c, 6, -6 ), At( c, 2, -2 ) );\n\t}\n\n\t/// \u003Csummary\u003EA body with a dashed sheet pulled tight around it.\u003C/summary\u003E\n\tprivate static void PaintShrinkwrap( Vector2 c, Color color )\n\t{\n\t\tFilled( color.WithAlpha( 0.35f ) );\n\t\tEditor.Paint.DrawCircle( At( c, 0, 1 ), 5.5f * _scale );\n\t\tStroked( color, 1.6f );\n\t\tvar prev = At( c, -8, 1 );\n\t\tfor ( var i = 1; i \u003C= 12; i\u002B\u002B )\n\t\t{\n\t\t\tvar a = MathF.PI \u002B i / 12f * MathF.PI;\n\t\t\tvar p = At( c, MathF.Cos( a ) * 8f, 1f \u002B MathF.Sin( a ) * 8f );\n\t\t\tif ( i % 2 == 1 )\n\t\t\t\tEditor.Paint.DrawLine( prev, p );\n\t\t\tprev = p;\n\t\t}\n\t\tEditor.Paint.DrawLine( At( c, -8, 1 ), At( c, -5.5f, 1 ) );\n\t\tEditor.Paint.DrawLine( At( c, 8, 1 ), At( c, 5.5f, 1 ) );\n\t}\n\n\t/// \u003Csummary\u003EA thin sheet and the same sheet given a thickness.\u003C/summary\u003E\n\tprivate static void PaintSolidify( Vector2 c, Color color )\n\t{\n\t\tStroked( color.WithAlpha( 0.5f ), 1.3f );\n\t\tEditor.Paint.DrawLine( At( c, -8, -6 ), At( c, 8, -6 ) );\n\t\tFilled( color.WithAlpha( 0.85f ) );\n\t\tEditor.Paint.DrawPolygon( At( c, -8, 1 ), At( c, 8, 1 ), At( c, 8, 7 ), At( c, -8, 7 ) );\n\t}\n\n\t/// \u003Csummary\u003ECloth pinned along the top and sagging under its own weight: two pins, a rail, and\n\t/// the catenary the fabric hangs in.\u003C/summary\u003E\n\t/// \u003Csummary\u003E\n\t/// A dressmaker\u0027s dummy: torso on a stand.\n\t///\n\t/// DELIBERATELY NOT A PERSON AND NOT A BONE. The button loads somebody else\u0027s finished model,\n\t/// and the two things the glyph has to carry are that it is a whole body and that the body is\n\t/// not what you are making. A figure says the first and denies the second; the rig glyphs next\n\t/// door say neither. A stand says both, and it is what the thing is actually for.\n\t/// \u003C/summary\u003E\n\t/// \u003Csummary\u003EA scalloped edge - the hem the tool makes, seen side on.\u003C/summary\u003E\n\tprivate static void PaintFrill( Vector2 c, Color color )\n\t{\n\t\t// The cloth the trim is sewn to: a plain band, so the scallop below reads as an addition to\n\t\t// an edge rather than as a shape in its own right.\n\t\tStroked( color.WithAlpha( 0.5f ), 1.5f );\n\t\tEditor.Paint.DrawLine( At( c, -8, -6 ), At( c, 8, -6 ) );\n\t\tEditor.Paint.DrawLine( At( c, -8, -6 ), At( c, -8, -1 ) );\n\t\tEditor.Paint.DrawLine( At( c, 8, -6 ), At( c, 8, -1 ) );\n\n\t\t// The seam it hangs from.\n\t\tStroked( color, 1.6f );\n\t\tEditor.Paint.DrawLine( At( c, -8, -1 ), At( c, 8, -1 ) );\n\n\t\t// Four scallops. Cosine off the seam, deepening as it falls - the same shape the generator\n\t\t// makes, for the same reason: the gather grows with the distance from the seam.\n\t\tStroked( color, 1.7f );\n\n\t\tvar prev = At( c, -8, -1 );\n\n\t\tfor ( var i = 1; i \u003C= 32; i\u002B\u002B )\n\t\t{\n\t\t\tvar t = i / 32f;\n\t\t\tvar x = -8f \u002B t * 16f;\n\t\t\tvar drop = 4f \u002B 2.6f * MathF.Cos( t * MathF.PI * 8f );\n\n\t\t\tvar p = At( c, x, -1f \u002B drop );\n\t\t\tEditor.Paint.DrawLine( prev, p );\n\t\t\tprev = p;\n\t\t}\n\t}\n\n\tprivate static void PaintWearer( Vector2 c, Color color )\n\t{\n\t\t// The torso: shoulders wide, waist in, hips out again. Drawn as a filled silhouette because\n\t\t// it is a solid object, unlike the wireframe operations either side of it on the bar.\n\t\tFilled( color );\n\n\t\tvar torso = new List\u003CVector2\u003E();\n\n\t\tfor ( var i = 0; i \u003C= 16; i\u002B\u002B )\n\t\t{\n\t\t\tvar t = i / 16f;\n\t\t\tvar y = -8f \u002B t * 11f;\n\n\t\t\t// Shoulder 5.2 -\u003E waist 3.0 -\u003E hip 4.4, as one smooth sweep rather than three segments.\n\t\t\tvar w = 5.2f - 2.2f * MathF.Sin( t * MathF.PI * 0.72f ) \u002B 2.4f * t * t;\n\n\t\t\ttorso.Add( At( c, w, y ) );\n\t\t}\n\n\t\tfor ( var i = 16; i \u003E= 0; i-- )\n\t\t{\n\t\t\tvar t = i / 16f;\n\t\t\tvar y = -8f \u002B t * 11f;\n\t\t\tvar w = 5.2f - 2.2f * MathF.Sin( t * MathF.PI * 0.72f ) \u002B 2.4f * t * t;\n\n\t\t\ttorso.Add( At( c, -w, y ) );\n\t\t}\n\n\t\tEditor.Paint.DrawPolygon( torso );\n\n\t\t// The neck stub, so the shoulders read as shoulders and not as the top of a vase.\n\t\tEditor.Paint.DrawRect( new Rect( At( c, -1.4f, -9.6f ), new Vector2( 2.8f * _scale, 2f * _scale ) ) );\n\n\t\t// The stand. Stroked rather than filled - it is furniture holding the subject up, and\n\t\t// drawing it as lightly as the pins in PaintDrape keeps the body the thing you see first.\n\t\tStroked( color, 1.6f );\n\t\tEditor.Paint.DrawLine( At( c, 0, 3 ), At( c, 0, 7.5f ) );\n\t\tEditor.Paint.DrawLine( At( c, -4, 8 ), At( c, 4, 8 ) );\n\t}\n\n\tprivate static void PaintDrape( Vector2 c, Color color )\n\t{\n\t\tStroked( color.WithAlpha( 0.5f ), 1.4f );\n\t\tEditor.Paint.DrawLine( At( c, -8, -6 ), At( c, 8, -6 ) );\n\n\t\t// The pins that hold it up - the selection, in the tool\u0027s own terms.\n\t\tFilled( color );\n\t\tEditor.Paint.DrawCircle( At( c, -6, -6 ), 1.8f * _scale );\n\t\tEditor.Paint.DrawCircle( At( c, 6, -6 ), 1.8f * _scale );\n\n\t\t// cosh, sampled: the shape a hanging sheet actually takes.\n\t\tStroked( color, 1.7f );\n\t\tvar prev = At( c, -6, -6 );\n\t\tfor ( var i = 1; i \u003C= 14; i\u002B\u002B )\n\t\t{\n\t\t\tvar t = -1f \u002B i / 7f;\n\t\t\tvar p = At( c, t * 6f, -6f \u002B (MathF.Cosh( 1.6f ) - MathF.Cosh( t * 1.6f )) * 4.6f );\n\t\t\tEditor.Paint.DrawLine( prev, p );\n\t\t\tprev = p;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EA swatch of woven cloth: the weave is what the fabric setting picks.\u003C/summary\u003E\n\tprivate static void PaintFabric( Vector2 c, Color color )\n\t{\n\t\tStroked( color.WithAlpha( 0.8f ), 1.5f );\n\t\tEditor.Paint.DrawLine( At( c, -7, -7 ), At( c, 7, -7 ) );\n\t\tEditor.Paint.DrawLine( At( c, -7, 7 ), At( c, 7, 7 ) );\n\t\tEditor.Paint.DrawLine( At( c, -7, -7 ), At( c, -7, 7 ) );\n\t\tEditor.Paint.DrawLine( At( c, 7, -7 ), At( c, 7, 7 ) );\n\n\t\tStroked( color.WithAlpha( 0.55f ), 1.2f );\n\t\tfor ( var i = 1; i \u003C= 2; i\u002B\u002B )\n\t\t{\n\t\t\tvar o = -7f \u002B i * 14f / 3f;\n\t\t\tEditor.Paint.DrawLine( At( c, o, -7 ), At( c, o, 7 ) );\n\t\t\tEditor.Paint.DrawLine( At( c, -7, o ), At( c, 7, o ) );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EA surface cut open along a line: the two halves parting, and the cut itself in the\n\t/// red every tool uses for a seam.\u003C/summary\u003E\n\tprivate static void PaintSeam( Vector2 c, Color color )\n\t{\n\t\tFilled( color.WithAlpha( 0.35f ) );\n\t\tEditor.Paint.DrawPolygon( At( c, -8, -7 ), At( c, -2, -7 ), At( c, -2, 7 ), At( c, -8, 7 ) );\n\t\tEditor.Paint.DrawPolygon( At( c, 2, -7 ), At( c, 8, -7 ), At( c, 8, 7 ), At( c, 2, 7 ) );\n\n\t\t// The cut. Red, because that is what a marked seam is drawn as in the viewport.\n\t\tStroked( new Color( 0.93f, 0.26f, 0.21f ), 2f );\n\t\tEditor.Paint.DrawLine( At( c, -2, -7 ), At( c, -2, 7 ) );\n\t\tEditor.Paint.DrawLine( At( c, 2, -7 ), At( c, 2, 7 ) );\n\t}\n\n\t/// \u003Csummary\u003EA box opened out into its net - the thing unwrapping does, in one picture.\u003C/summary\u003E\n\tprivate static void PaintUnwrap( Vector2 c, Color color )\n\t{\n\t\tFilled( color.WithAlpha( 0.75f ) );\n\t\tEditor.Paint.DrawPolygon( At( c, -2.5f, -2.5f ), At( c, 2.5f, -2.5f ), At( c, 2.5f, 2.5f ), At( c, -2.5f, 2.5f ) );\n\n\t\t// The four flaps folded out around it.\n\t\tFilled( color.WithAlpha( 0.35f ) );\n\t\tEditor.Paint.DrawPolygon( At( c, -2.5f, -8 ), At( c, 2.5f, -8 ), At( c, 2.5f, -3.5f ), At( c, -2.5f, -3.5f ) );\n\t\tEditor.Paint.DrawPolygon( At( c, -2.5f, 3.5f ), At( c, 2.5f, 3.5f ), At( c, 2.5f, 8 ), At( c, -2.5f, 8 ) );\n\t\tEditor.Paint.DrawPolygon( At( c, -8, -2.5f ), At( c, -3.5f, -2.5f ), At( c, -3.5f, 2.5f ), At( c, -8, 2.5f ) );\n\t\tEditor.Paint.DrawPolygon( At( c, 3.5f, -2.5f ), At( c, 8, -2.5f ), At( c, 8, 2.5f ), At( c, 3.5f, 2.5f ) );\n\t}\n}\n"},{"Ident":"pooh.geppetto","Path":"Editor/EffigyEditor/EffigyPreview.cs","FileName":"EffigyPreview.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":411088,"IsPrivate":false,"Code":"using Effigy;\r\nusing Sandbox;\r\nusing System;\r\nusing System.Collections.Generic;\r\n\r\nnamespace Marionette.EditorTools;\r\n\r\n/// \u003Csummary\u003E\r\n/// Turns the Part Studio\u0027s in-memory PolyMesh straight into a runtime Model, so the viewport can\r\n/// show the result of a feature the moment it is added.\r\n///\r\n/// The alternative \u2014 the path Export/Compile still take \u2014 is OBJ on disk, a generated .vmdl, and\r\n/// a call into the asset compiler. That takes hundreds of milliseconds and writes files, which is\r\n/// fine for producing a placeable prop and hopeless as the response to dragging a slider. This\r\n/// path never touches the disk.\r\n///\r\n/// It is deliberately NOT a second geometry pipeline: it shares MeshNormals with ObjWriter, so\r\n/// what you see here and what the compiler bakes are smoothed by the same rule.\r\n///\r\n/// MATERIALS RENDER HERE NOW. A face carries a slot number and PartStudio.MaterialNames binds the\r\n/// slot to a vmat, so the preview groups faces by the material they resolve to and builds one\r\n/// submesh per material \u2014 the same grouping the exporters express as usemtl runs. Faces on slot 0,\r\n/// on a slot nothing is bound to, or on a slot whose vmat will not load fall back to the flat\r\n/// placeholder rather than rendering as nothing. Pass no resolver \u2014 the sculpt preview does \u2014 and\r\n/// the whole model is the placeholder, which is the old behaviour.\r\n/// \u003C/summary\u003E\r\ninternal static class EffigyPreview\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// FLAT grey, with no pattern on it at all. The fallback for a face whose slot names no\r\n\t/// material, or names one that will not load.\r\n\t///\r\n\t/// It used to be dev/reflectivity_30, and that material actively lies about scale: its texture\r\n\t/// is a grid with the number \u002230\u0022 printed in every tile - the material\u0027s reflectivity, nothing\r\n\t/// to do with size - and caps take plane coordinates straight through as UVs, so it tiles once\r\n\t/// per sketch unit. A 30x30 face therefore came out covered in thirty-odd squares each labelled\r\n\t/// \u002230\u0022, which reads as a part 900 units across. gray_50\u0027s texture is a single flat colour.\r\n\t/// \u003C/summary\u003E\r\n\tprivate const string PreviewMaterial = \u0022materials/dev/gray_50.vmat\u0022;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Build a Model from the mesh, optionally resolving each face\u0027s material slot to a real vmat.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022materialForSlot\u0022\u003ESlot number to bound material path, or null / empty for an\r\n\t/// unbound slot. Pass null to render everything on the placeholder.\u003C/param\u003E\r\n\tpublic static Model Build( PolyMesh mesh, Func\u003Cint, string\u003E materialForSlot = null,\r\n\t\tfloat smoothingAngleDegrees = MeshNormals.DefaultSmoothingAngleDegrees )\r\n\t{\r\n\t\tif ( mesh is null || mesh.FaceCount == 0 || mesh.VertexCount == 0 )\r\n\t\t\treturn null;\r\n\r\n\t\tvar (cornerNormals, normals) = MeshNormals.ComputeCornerNormals( mesh, smoothingAngleDegrees );\r\n\r\n\t\tvar placeholder = Material.Load( PreviewMaterial );\r\n\t\tvar bounds = BoundsOf( mesh );\r\n\r\n\t\t// Faces bucketed by the material they render with. One drop of brushed steel onto three\r\n\t\t// faces is one bucket and one submesh; a part nobody assigned is one bucket on the\r\n\t\t// placeholder. The slot is resolved once and cached, not once per face.\r\n\t\tvar buckets = new Dictionary\u003CMaterial, List\u003Cint\u003E\u003E();\r\n\t\tvar slotCache = new Dictionary\u003Cint, Material\u003E();\r\n\r\n\t\tfor ( var fi = 0; fi \u003C mesh.FaceCount; fi\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( mesh.Faces[fi].Count \u003C 3 )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar material = ResolveMaterial( mesh.Faces[fi].Material, materialForSlot, placeholder, slotCache );\r\n\r\n\t\t\tif ( !buckets.TryGetValue( material, out var faces ) )\r\n\t\t\t\tbuckets[material] = faces = new List\u003Cint\u003E();\r\n\r\n\t\t\tfaces.Add( fi );\r\n\t\t}\r\n\r\n\t\tvar builder = Model.Builder;\r\n\t\tvar any = false;\r\n\r\n\t\tforeach ( var (material, faces) in buckets )\r\n\t\t{\r\n\t\t\tif ( BuildSubmesh( mesh, faces, cornerNormals, normals, material, bounds ) is { } sub )\r\n\t\t\t{\r\n\t\t\t\tbuilder.AddMesh( sub );\r\n\t\t\t\tany = true;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn any ? builder.Create() : null;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Build a Model from the mesh wearing ONE material on every face, slot resolution ignored.\r\n\t///\r\n\t/// THIS IS THE PAINT-PREVIEW PATH. While painting, the whole part wears the painted atlas \u2014 a\r\n\t/// \u003Csee cref=\u0022Material.CreateCopy\u0022/\u003E with the live canvas texture bound \u2014 so the slot bucketing\r\n\t/// above is the wrong answer there. One material, one submesh, the same smoothing as the ordinary\r\n\t/// preview, and the caller owns the material\u0027s lifetime across rebuilds.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static Model Build( PolyMesh mesh, Material material,\r\n\t\tfloat smoothingAngleDegrees = MeshNormals.DefaultSmoothingAngleDegrees )\r\n\t{\r\n\t\tif ( mesh is null || mesh.FaceCount == 0 || mesh.VertexCount == 0 || material is null )\r\n\t\t\treturn null;\r\n\r\n\t\tvar (cornerNormals, normals) = MeshNormals.ComputeCornerNormals( mesh, smoothingAngleDegrees );\r\n\t\tvar bounds = BoundsOf( mesh );\r\n\r\n\t\tvar faces = new List\u003Cint\u003E( mesh.FaceCount );\r\n\r\n\t\tfor ( var fi = 0; fi \u003C mesh.FaceCount; fi\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( mesh.Faces[fi].Count \u003E= 3 )\r\n\t\t\t\tfaces.Add( fi );\r\n\t\t}\r\n\r\n\t\tvar sub = BuildSubmesh( mesh, faces, cornerNormals, normals, material, bounds );\r\n\r\n\t\tif ( sub is null )\r\n\t\t\treturn null;\r\n\r\n\t\tvar builder = Model.Builder;\r\n\t\tbuilder.AddMesh( sub );\r\n\r\n\t\treturn builder.Create();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// A preview Model that can be rewritten in place when only vertex positions change \u2014 a body\r\n\t/// being dragged by the Transform handle. Rebuilding the Model, its buffers and its GameObject\r\n\t/// every frame is what made dragging a dense import sluggish, so this retains the meshes and\r\n\t/// overwrites their vertex buffers instead. Buckets, triangulation and buffer allocation are\r\n\t/// done once; a drag only recomputes normals and re-uploads the vertex data.\r\n\t/// \u003C/summary\u003E\r\n\tinternal sealed class LivePreview\r\n\t{\r\n\t\tpublic Model Model { get; private set; }\r\n\r\n\t\tprivate readonly List\u003CMesh\u003E _meshes = new();\r\n\t\tprivate readonly List\u003CList\u003CSimpleVertex\u003E\u003E _vertices = new();\r\n\t\tprivate readonly Dictionary\u003CMaterial, int\u003E _bucketIndex = new();\r\n\t\tprivate int[] _faceMesh;\r\n\t\tprivate int[] _faceCorners;\r\n\t\tprivate int _faceCount;\r\n\r\n\t\t// The corner normals the vertex buffers were last built with. Kept so a position-only update\r\n\t\t// (a sculpt stroke, which moves vertices without changing topology) can reuse them instead of\r\n\t\t// re-running the O(vertices) fan pass every dab. The normals snap correct on the next full\r\n\t\t// update.\r\n\t\tprivate int[][] _cornerNormals;\r\n\t\tprivate List\u003CVec3\u003E _normals;\r\n\r\n\t\tprivate LivePreview() { }\r\n\r\n\t\tpublic static LivePreview Build( PolyMesh mesh, Func\u003Cint, string\u003E materialForSlot,\r\n\t\t\tfloat smoothingAngleDegrees )\r\n\t\t{\r\n\t\t\tvar live = new LivePreview { _faceCount = mesh.FaceCount };\r\n\t\t\tlive.Rebuild( mesh, materialForSlot, smoothingAngleDegrees );\r\n\t\t\treturn live;\r\n\t\t}\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// Rewrite the vertex buffers from a mesh whose topology matches the one this was built\r\n\t\t/// from. Returns false \u2014 the caller must rebuild \u2014 when the face count, material layout or\r\n\t\t/// corner count changed, because any of those moves the bucketing and the index buffers.\r\n\t\t/// \u003C/summary\u003E\r\n\t\tpublic bool TryUpdate( PolyMesh mesh, Func\u003Cint, string\u003E materialForSlot, float smoothingAngleDegrees )\r\n\t\t{\r\n\t\t\tif ( Model is null || mesh.FaceCount != _faceCount || mesh.HasVertexColors )\r\n\t\t\t\treturn false;\r\n\r\n\t\t\tvar placeholder = Material.Load( PreviewMaterial );\r\n\t\t\tvar slotCache = new Dictionary\u003Cint, Material\u003E();\r\n\t\t\tvar faceLists = new List\u003CList\u003Cint\u003E\u003E( _meshes.Count );\r\n\r\n\t\t\tfor ( var i = 0; i \u003C _meshes.Count; i\u002B\u002B )\r\n\t\t\t\tfaceLists.Add( new List\u003Cint\u003E() );\r\n\r\n\t\t\tfor ( var fi = 0; fi \u003C mesh.FaceCount; fi\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\t// The index buffers are laid out corner by corner, so two faces swapping corner\r\n\t\t\t\t// counts keeps the per-bucket vertex total the same while scrambling the triangles.\r\n\t\t\t\tif ( mesh.Faces[fi].Count != _faceCorners[fi] )\r\n\t\t\t\t\treturn false;\r\n\r\n\t\t\t\tif ( mesh.Faces[fi].Count \u003C 3 )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tvar material = ResolveMaterial( mesh.Faces[fi].Material, materialForSlot, placeholder, slotCache );\r\n\r\n\t\t\t\t// A face on a bucket that was not there at build time means the material layout\r\n\t\t\t\t// changed, which an in-place update cannot express.\r\n\t\t\t\tif ( !_bucketIndex.TryGetValue( material, out var bi ) || bi != _faceMesh[fi] )\r\n\t\t\t\t\treturn false;\r\n\r\n\t\t\t\tfaceLists[bi].Add( fi );\r\n\t\t\t}\r\n\r\n\t\t\tvar (cornerNormals, normals) = MeshNormals.ComputeCornerNormals( mesh, smoothingAngleDegrees );\r\n\t\t\t_cornerNormals = cornerNormals;\r\n\t\t\t_normals = normals;\r\n\t\t\tvar bounds = BoundsOf( mesh );\r\n\r\n\t\t\tfor ( var i = 0; i \u003C _meshes.Count; i\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tvar vertices = BuildVertices( mesh, faceLists[i], cornerNormals, normals );\r\n\r\n\t\t\t\tif ( vertices.Count != _vertices[i].Count )\r\n\t\t\t\t\treturn false;\r\n\r\n\t\t\t\t_meshes[i].SetVertexBufferData( vertices, 0 );\r\n\t\t\t\t_meshes[i].Bounds = bounds;\r\n\t\t\t}\r\n\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\t/// \u003Csummary\u003E\r\n\t\t/// Rewrite positions only, reusing the normals and UVs the buffers were last built with. A\r\n\t\t/// sculpt stroke moves vertices without changing topology, and this skips the O(vertices)\r\n\t\t/// corner-normal fan pass that a full update re-runs every dab \u2014 the normals snap correct on\r\n\t\t/// the next full update (stroke end). Returns false when the face layout has changed, which\r\n\t\t/// means a full rebuild instead.\r\n\t\t/// \u003C/summary\u003E\r\n\t\tpublic bool TryUpdatePositions( PolyMesh mesh )\r\n\t\t{\r\n\t\t\tif ( Model is null || mesh.FaceCount != _faceCount || _cornerNormals is null || mesh.HasVertexColors )\r\n\t\t\t\treturn false;\r\n\r\n\t\t\tvar faceLists = new List\u003CList\u003Cint\u003E\u003E( _meshes.Count );\r\n\r\n\t\t\tfor ( var i = 0; i \u003C _meshes.Count; i\u002B\u002B )\r\n\t\t\t\tfaceLists.Add( new List\u003Cint\u003E() );\r\n\r\n\t\t\tfor ( var fi = 0; fi \u003C mesh.FaceCount; fi\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tif ( mesh.Faces[fi].Count != _faceCorners[fi] )\r\n\t\t\t\t\treturn false;\r\n\r\n\t\t\t\tif ( mesh.Faces[fi].Count \u003C 3 )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tfaceLists[_faceMesh[fi]].Add( fi );\r\n\t\t\t}\r\n\r\n\t\t\tfor ( var i = 0; i \u003C _meshes.Count; i\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tvar vertices = BuildVertices( mesh, faceLists[i], _cornerNormals, _normals );\r\n\r\n\t\t\t\tif ( vertices.Count != _vertices[i].Count )\r\n\t\t\t\t\treturn false;\r\n\r\n\t\t\t\t_meshes[i].SetVertexBufferData( vertices, 0 );\r\n\t\t\t\t_vertices[i] = vertices;\r\n\t\t\t}\r\n\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\tvoid Rebuild( PolyMesh mesh, Func\u003Cint, string\u003E materialForSlot, float smoothingAngleDegrees )\r\n\t\t{\r\n\t\t\t_meshes.Clear();\r\n\t\t\t_vertices.Clear();\r\n\t\t\t_bucketIndex.Clear();\r\n\r\n\t\t\tvar (cornerNormals, normals) = MeshNormals.ComputeCornerNormals( mesh, smoothingAngleDegrees );\r\n\t\t\t_cornerNormals = cornerNormals;\r\n\t\t\t_normals = normals;\r\n\t\t\tvar bounds = BoundsOf( mesh );\r\n\t\t\tvar placeholder = Material.Load( PreviewMaterial );\r\n\t\t\tvar slotCache = new Dictionary\u003Cint, Material\u003E();\r\n\r\n\t\t\tvar buckets = new List\u003C(Material Material, List\u003Cint\u003E Faces)\u003E();\r\n\t\t\t_faceMesh = new int[mesh.FaceCount];\r\n\t\t\t_faceCorners = new int[mesh.FaceCount];\r\n\r\n\t\t\tfor ( var fi = 0; fi \u003C mesh.FaceCount; fi\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\t_faceCorners[fi] = mesh.Faces[fi].Count;\r\n\r\n\t\t\t\tif ( mesh.Faces[fi].Count \u003C 3 )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tvar material = ResolveMaterial( mesh.Faces[fi].Material, materialForSlot, placeholder, slotCache );\r\n\r\n\t\t\t\tif ( !_bucketIndex.TryGetValue( material, out var bi ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tbi = buckets.Count;\r\n\t\t\t\t\t_bucketIndex[material] = bi;\r\n\t\t\t\t\tbuckets.Add( (material, new List\u003Cint\u003E()) );\r\n\t\t\t\t}\r\n\r\n\t\t\t\tbuckets[bi].Faces.Add( fi );\r\n\t\t\t\t_faceMesh[fi] = bi;\r\n\t\t\t}\r\n\r\n\t\t\tvar builder = Model.Builder;\r\n\r\n\t\t\tforeach ( var (material, faces) in buckets )\r\n\t\t\t{\r\n\t\t\t\tvar vertices = BuildVertices( mesh, faces, cornerNormals, normals );\r\n\r\n\t\t\t\tif ( vertices.Count == 0 )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tvar indices = TriangulateIndices( mesh, faces );\r\n\t\t\t\tvar sbMesh = new Mesh( material );\r\n\r\n\t\t\t\tif ( mesh.HasVertexColors )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar coloured = BuildColouredVertices( mesh, faces, cornerNormals, normals );\r\n\t\t\t\t\tsbMesh.CreateVertexBuffer\u003CVertex\u003E( coloured.Count, coloured );\r\n\t\t\t\t}\r\n\t\t\t\telse\r\n\t\t\t\t\tsbMesh.CreateVertexBuffer\u003CSimpleVertex\u003E( vertices.Count, vertices );\r\n\t\t\t\tsbMesh.CreateIndexBuffer( indices.Count, indices );\r\n\t\t\t\tsbMesh.Bounds = bounds;\r\n\r\n\t\t\t\tbuilder.AddMesh( sbMesh );\r\n\t\t\t\t_meshes.Add( sbMesh );\r\n\t\t\t\t_vertices.Add( vertices );\r\n\t\t\t}\r\n\r\n\t\t\tModel = _meshes.Count \u003E 0 ? builder.Create() : null;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The material a slot renders with: the vmat bound to it, or the flat placeholder.\r\n\t///\r\n\t/// Slot 0 is the slot every face starts on and never carries a material, so it is the\r\n\t/// placeholder without a lookup. A named slot whose vmat will not load \u2014 a path into a package\r\n\t/// that is not installed, a material deleted since it was assigned \u2014 also falls back rather\r\n\t/// than rendering the part as nothing, which is the failure the single-material preview used\r\n\t/// to avoid wholesale.\r\n\t/// \u003C/summary\u003E\r\n\tprivate static Material ResolveMaterial( int slot, Func\u003Cint, string\u003E materialForSlot,\r\n\t\tMaterial placeholder, Dictionary\u003Cint, Material\u003E cache )\r\n\t{\r\n\t\tif ( slot \u003C= 0 || materialForSlot is null )\r\n\t\t\treturn placeholder;\r\n\r\n\t\tif ( cache.TryGetValue( slot, out var cached ) )\r\n\t\t\treturn cached;\r\n\r\n\t\tvar name = materialForSlot( slot );\r\n\t\tvar material = string.IsNullOrWhiteSpace( name ) ? null : Material.Load( name );\r\n\r\n\t\t// A named slot whose vmat would not load is the one case the fallback hides \u2014 the face\r\n\t\t// renders as placeholder grey and nothing says why. Say why, once per slot per build.\r\n\t\tif ( material is null \u0026\u0026 !string.IsNullOrWhiteSpace( name ) )\r\n\t\t\tLog.Warning( $\u0022[effigy-preview] slot {slot}: material \u0027{name}\u0027 failed to load \u2014 using placeholder\u0022 );\r\n\r\n\t\treturn cache[slot] = material ?? placeholder;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// One Mesh over a subset of the faces, all sharing one material.\r\n\t///\r\n\t/// ONE VERTEX FORMAT, since paint stopped being vertex colour. There was a second path here that\r\n\t/// built the richer \u003Cc\u003EVertex\u003C/c\u003E so the engine could multiply a material by a per-vertex colour;\r\n\t/// it was chosen by \u003Cc\u003EPolyMesh.HasVertexColors\u003C/c\u003E, which nothing sets any more, so it rendered\r\n\t/// nothing that this path does not. Paint reaches the viewport as the painted body\u0027s material\r\n\t/// now, the same way a dropped material does.\r\n\t/// \u003C/summary\u003E\r\n\tprivate static Mesh BuildSubmesh( PolyMesh mesh, List\u003Cint\u003E faceIndices, int[][] cornerNormals,\r\n\t\tList\u003CVec3\u003E normals, Material material, BBox bounds )\r\n\t{\r\n\t\tvar vertices = BuildVertices( mesh, faceIndices, cornerNormals, normals );\r\n\t\tvar indices = TriangulateIndices( mesh, faceIndices );\r\n\r\n\t\tif ( indices.Count == 0 )\r\n\t\t\treturn null;\r\n\r\n\t\tvar sbMesh = new Mesh( material );\r\n\r\n\t\tif ( mesh.HasVertexColors )\r\n\t\t{\r\n\t\t\tvar coloured = BuildColouredVertices( mesh, faceIndices, cornerNormals, normals );\r\n\t\t\tsbMesh.CreateVertexBuffer\u003CVertex\u003E( coloured.Count, coloured );\r\n\t\t}\r\n\t\telse\r\n\t\t\tsbMesh.CreateVertexBuffer\u003CSimpleVertex\u003E( vertices.Count, vertices );\r\n\r\n\t\tsbMesh.CreateIndexBuffer( indices.Count, indices );\r\n\t\tsbMesh.Bounds = bounds;\r\n\r\n\t\treturn sbMesh;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The same corners as \u003Csee cref=\u0022BuildVertices\u0022/\u003E, carrying the mesh\u0027s vertex colour.\r\n\t///\r\n\t/// BACK FOR FUR. The vertex-colour path was retired when paint became a texture; it returns\r\n\t/// because s\u0026box\u0027s fur.shader reads the mesh COLOR stream - red is each shell\u0027s height - and\r\n\t/// without it every shell reads as the root layer and the fur renders as a solid lump. Every other\r\n\t/// shader ignores the stream, so the rest of a furred part looks exactly as it did.\r\n\t/// \u003C/summary\u003E\r\n\tprivate static List\u003CVertex\u003E BuildColouredVertices( PolyMesh mesh, List\u003Cint\u003E faceIndices,\r\n\t\tint[][] cornerNormals, List\u003CVec3\u003E normals )\r\n\t{\r\n\t\tvar colours = mesh.VertexColors;\r\n\t\tvar vertices = new List\u003CVertex\u003E( faceIndices.Count * 4 );\r\n\r\n\t\tforeach ( var fi in faceIndices )\r\n\t\t{\r\n\t\t\tvar face = mesh.Faces[fi];\r\n\t\t\tvar corners = cornerNormals[fi];\r\n\r\n\t\t\tfor ( var c = 0; c \u003C face.Count; c\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tvar p = mesh.Positions[face.Indices[c]];\r\n\t\t\t\tvar n = normals[corners[c]];\r\n\t\t\t\tvar uv = face.UVs is not null \u0026\u0026 c \u003C face.UVs.Length ? face.UVs[c] : default;\r\n\t\t\t\tvar tint = colours[face.Indices[c]].Tint();\r\n\t\t\t\tvar normal = new Vector3( n.x, n.y, n.z );\r\n\t\t\t\tvar tangent = MathF.Abs( normal.z ) \u003C 0.9f\r\n\t\t\t\t\t? Vector3.Cross( Vector3.Up, normal ).Normal\r\n\t\t\t\t\t: Vector3.Cross( Vector3.Forward, normal ).Normal;\r\n\r\n\t\t\t\tvertices.Add( new Vertex\r\n\t\t\t\t{\r\n\t\t\t\t\tPosition = new Vector3( p.x, p.y, p.z ),\r\n\t\t\t\t\tNormal = normal,\r\n\t\t\t\t\tTangent = new Vector4( tangent, 1f ),\r\n\t\t\t\t\tTexCoord0 = new Vector2( uv.x, uv.y ),\r\n\t\t\t\t\tColor = new Color32(\r\n\t\t\t\t\t\t(byte)MathF.Round( Math.Clamp( tint.x, 0f, 1f ) * 255f ),\r\n\t\t\t\t\t\t(byte)MathF.Round( Math.Clamp( tint.y, 0f, 1f ) * 255f ),\r\n\t\t\t\t\t\t(byte)MathF.Round( Math.Clamp( tint.z, 0f, 1f ) * 255f ),\r\n\t\t\t\t\t\t255 ),\r\n\t\t\t\t} );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn vertices;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// One vertex per face corner rather than per position, in the same order\r\n\t/// \u003Csee cref=\u0022TriangulateIndices\u0022/\u003E indexes them. Corner normals are the whole point of\r\n\t/// MeshNormals - sharing a vertex between two faces that disagree about the normal is exactly\r\n\t/// what rounds off a box\u0027s edges.\r\n\t/// \u003C/summary\u003E\r\n\tprivate static List\u003CSimpleVertex\u003E BuildVertices( PolyMesh mesh, List\u003Cint\u003E faceIndices,\r\n\t\tint[][] cornerNormals, List\u003CVec3\u003E normals )\r\n\t{\r\n\t\tvar vertices = new List\u003CSimpleVertex\u003E( faceIndices.Count * 4 );\r\n\r\n\t\tforeach ( var fi in faceIndices )\r\n\t\t{\r\n\t\t\tvar face = mesh.Faces[fi];\r\n\t\t\tvar corners = cornerNormals[fi];\r\n\r\n\t\t\tfor ( var c = 0; c \u003C face.Count; c\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tvar p = mesh.Positions[face.Indices[c]];\r\n\t\t\t\tvar n = normals[corners[c]];\r\n\t\t\t\tvar uv = face.UVs is not null \u0026\u0026 c \u003C face.UVs.Length ? face.UVs[c] : default;\r\n\r\n\t\t\t\tvar position = new Vector3( p.x, p.y, p.z );\r\n\t\t\t\tvar normal = new Vector3( n.x, n.y, n.z );\r\n\r\n\t\t\t\tvertices.Add( new SimpleVertex( position, normal, Vector3.Forward, new Vector2( uv.x, uv.y ) ) );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn vertices;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Ear-clipping, not a fan. This used to fan from corner 0 on the grounds that every face the\r\n\t/// kernel produces is convex. Extrude caps are not: they are whatever closed region was drawn,\r\n\t/// and fanning a concave one fills its notches in \u2014 draw a dart and the solid came back as a\r\n\t/// quadrilateral with the concave corner swallowed.\r\n\t///\r\n\t/// A TRIANGLE BYPASSES THE CLIPPER. An import is overwhelmingly triangles, and ear clipping a\r\n\t/// 3-corner face is still a Newell normal, a flatten, a signed-area and several allocations per\r\n\t/// face \u2014 millions of them on a dense import. A triangle is always itself, so it is emitted\r\n\t/// straight through.\r\n\t/// \u003C/summary\u003E\r\n\tprivate static List\u003Cint\u003E TriangulateIndices( PolyMesh mesh, List\u003Cint\u003E faceIndices )\r\n\t{\r\n\t\tvar indices = new List\u003Cint\u003E( faceIndices.Count * 6 );\r\n\t\tvar first = 0;\r\n\r\n\t\tforeach ( var fi in faceIndices )\r\n\t\t{\r\n\t\t\tvar face = mesh.Faces[fi];\r\n\r\n\t\t\tif ( face.Count == 3 )\r\n\t\t\t{\r\n\t\t\t\tindices.Add( first );\r\n\t\t\t\tindices.Add( first \u002B 1 );\r\n\t\t\t\tindices.Add( first \u002B 2 );\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tvar polygon = new List\u003CVec3\u003E( face.Count );\r\n\r\n\t\t\t\tfor ( var k = 0; k \u003C face.Count; k\u002B\u002B )\r\n\t\t\t\t\tpolygon.Add( mesh.Positions[face.Indices[k]] );\r\n\r\n\t\t\t\tforeach ( var (a, b, cc) in Triangulate.Face( polygon ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tindices.Add( first \u002B a );\r\n\t\t\t\t\tindices.Add( first \u002B b );\r\n\t\t\t\t\tindices.Add( first \u002B cc );\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tfirst \u002B= face.Count;\r\n\t\t}\r\n\r\n\t\treturn indices;\r\n\t}\r\n\r\n\tprivate static BBox BoundsOf( PolyMesh mesh )\r\n\t{\r\n\t\tvar min = new Vector3( float.MaxValue );\r\n\t\tvar max = new Vector3( float.MinValue );\r\n\r\n\t\tforeach ( var p in mesh.Positions )\r\n\t\t{\r\n\t\t\tvar v = new Vector3( p.x, p.y, p.z );\r\n\t\t\tmin = Vector3.Min( min, v );\r\n\t\t\tmax = Vector3.Max( max, v );\r\n\t\t}\r\n\r\n\t\treturn new BBox( min, max );\r\n\t}\r\n}\r\n"},{"Ident":"pooh.geppetto","Path":"Editor/EffigyEditor/EffigySketchGridBar.cs","FileName":"EffigySketchGridBar.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":411088,"IsPrivate":false,"Code":"using Editor;\nusing System;\n\nnamespace Marionette.EditorTools;\n\n/// \u003Csummary\u003E\n/// The grid switch and its spacing, floating in the corner of the viewport while a sketch is open.\n///\n/// WHY IT IS NOT ONLY IN SETTINGS. Both controls already live in Edit \u0026gt; Settings, and that is the\n/// right home for them when you are setting up how the tool behaves. It is the wrong home for them\n/// while you are drawing: the grid is the paper, changing paper is part of drawing, and a dialog\n/// three menus away is not somewhere you go mid-sketch. Every sketcher worth using puts these two\n/// on the canvas.\n///\n/// NOT A SECOND COPY OF THE VALUE. This reads and writes the viewport\u0027s own properties - the same\n/// two the settings window sets - and re-reads them every time it is shown, so the two controls can\n/// never disagree about what is true. The spacing list itself is EffigySettingsWindow\u0027s, for the\n/// same reason: two lists drift, and a value that exists on one dropdown and not the other reads as\n/// the setting having been lost.\n///\n/// AT THE END OF THE TOOL ROW, not floating on the model. It belongs to the mode you are in, the\n/// way the Line and Rectangle buttons beside it do, and chrome is where a mode\u0027s controls go - a\n/// panel sitting on the part is one more thing between you and the thing you are drawing. Flush\n/// right, so the tools growing and shrinking as you change stage never move it.\n/// \u003C/summary\u003E\ninternal sealed class EffigySketchGridBar : Widget\n{\n\t/// \u003Csummary\u003EThe tool buttons\u0027 own height, so the two read as one row of controls rather than\n\t/// as a panel parked next to them.\u003C/summary\u003E\n\tpublic const float BarHeight = EffigyToolChrome.ButtonHeight;\n\tpublic const float BarWidth = 210f;\n\n\tprivate readonly Button _toggle;\n\tprivate readonly ComboBox _spacing;\n\n\t/// \u003Csummary\u003ESet while Refresh writes the controls. Both of them fire their change callbacks on\n\t/// assignment exactly as a click does, and without this a refresh would turn straight round and\n\t/// write the value it had only just read - harmless for the toggle, and for the dropdown a way\n\t/// to reset the spacing to whatever happened to be first.\u003C/summary\u003E\n\tprivate bool _syncing;\n\n\t/// \u003Csummary\u003ERaised after either control has changed the viewport, so the window can save the\n\t/// setting the same way the settings dialog\u0027s own callback does. The viewport is already\n\t/// updated by the time this fires.\u003C/summary\u003E\n\tpublic Action Changed { get; set; }\n\n\tprivate readonly EffigyViewport _viewport;\n\n\tpublic EffigySketchGridBar( Widget parent, EffigyViewport viewport ) : base( parent )\n\t{\n\t\t_viewport = viewport;\n\n\t\t// The same two flags every floating widget in this tool sets \u2014 a plain Widget paints the\n\t\t// system background, which is a white slab on the 3D view.\n\t\tTranslucentBackground = true;\n\t\tNoSystemBackground = true;\n\n\t\tVisible = false;\n\t\tFixedHeight = BarHeight;\n\t\tFixedWidth = BarWidth;\n\n\t\tLayout = Layout.Row();\n\t\tLayout.Spacing = 6;\n\t\tLayout.Margin = new Sandbox.UI.Margin( 6, 2, 6, 2 );\n\n\t\t_toggle = new Button( \u0022Grid\u0022, \u0022grid_on\u0022, this )\n\t\t{\n\t\t\tToolTip = \u0022Draw the grid on the face you are sketching on. The lines are the intervals \u0022\n\t\t\t\t\u002B \u0022the cursor snaps to.\u0022,\n\t\t\tFixedWidth = 74f,\n\t\t};\n\n\t\t_toggle.Clicked = ToggleGrid;\n\n\t\tLayout.Add( _toggle );\n\n\t\t_spacing = new ComboBox( this )\n\t\t{\n\t\t\tToolTip = \u0022How far apart the grid lines sit, in sketch units. Automatic fits the step \u0022\n\t\t\t\t\u002B \u0022to the face you are on and to how close the camera is.\u0022,\n\t\t};\n\n\t\tforeach ( var value in EffigySettingsWindow.Spacings )\n\t\t{\n\t\t\tvar step = value;\n\n\t\t\t_spacing.AddItem( EffigySettingsWindow.Describe( step ), onSelected: () =\u003E SetSpacing( step ) );\n\t\t}\n\n\t\tLayout.Add( _spacing, 1 );\n\n\t\tRefresh();\n\t}\n\n\t/// \u003Csummary\u003EThe tool row\u0027s background, so the gaps between the two controls disappear into the\n\t/// bar rather than showing as a panel laid on it. A widget cannot simply decline to paint: a\n\t/// rect it leaves alone keeps whatever the previous frame put there.\u003C/summary\u003E\n\tpublic Color GapColor { get; set; } = Theme.ControlBackground;\n\n\tprotected override void OnPaint()\n\t{\n\t\tPaint.ClearPen();\n\t\tPaint.SetBrush( GapColor );\n\t\tPaint.DrawRect( LocalRect );\n\t}\n\n\t/// \u003Csummary\u003EPoint both controls at what the viewport actually holds. Called on every show, and\n\t/// again whenever the settings window has been the one to change it.\u003C/summary\u003E\n\tpublic void Refresh()\n\t{\n\t\tif ( !_viewport.IsValid )\n\t\t\treturn;\n\n\t\t_syncing = true;\n\n\t\ttry\n\t\t{\n\t\t\tvar on = _viewport.ShowPlaneGrid;\n\n\t\t\t_toggle.Icon = on ? \u0022grid_on\u0022 : \u0022grid_off\u0022;\n\n\t\t\t// Tinted rather than labelled on and off. The button says \u0022Grid\u0022 either way, so the\n\t\t\t// only thing that has to change is whether it reads as engaged - and a label that\n\t\t\t// alternates between two words is a control you have to read to use.\n\t\t\t_toggle.Tint = on ? Theme.Blue : Theme.TextControl.WithAlpha( 0.5f );\n\n\t\t\tvar spacing = _viewport.GridSpacing;\n\t\t\tvar index = 0;\n\n\t\t\tfor ( var i = 0; i \u003C EffigySettingsWindow.Spacings.Length; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tif ( MathF.Abs( EffigySettingsWindow.Spacings[i] - spacing ) \u003C 0.0001f )\n\t\t\t\t{\n\t\t\t\t\tindex = i;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t_spacing.CurrentIndex = index;\n\t\t}\n\t\tfinally\n\t\t{\n\t\t\t_syncing = false;\n\t\t}\n\t}\n\n\tprivate void ToggleGrid()\n\t{\n\t\tif ( _syncing || !_viewport.IsValid )\n\t\t\treturn;\n\n\t\t_viewport.ShowPlaneGrid = !_viewport.ShowPlaneGrid;\n\n\t\tRefresh();\n\t\tChanged?.Invoke();\n\t}\n\n\tprivate void SetSpacing( float step )\n\t{\n\t\tif ( _syncing || !_viewport.IsValid )\n\t\t\treturn;\n\n\t\t_viewport.GridSpacing = step;\n\n\t\t// Choosing a spacing while the grid is off is asking to see it. Turning it on is what was\n\t\t// meant, and leaving it off would make the dropdown look broken.\n\t\tif ( !_viewport.ShowPlaneGrid )\n\t\t\t_viewport.ShowPlaneGrid = true;\n\n\t\tRefresh();\n\t\tChanged?.Invoke();\n\t}\n}\n"},{"Ident":"pooh.geppetto","Path":"Editor/EffigyEditor/EffigyViewport.MaterialBrush.cs","FileName":"EffigyViewport.MaterialBrush.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":411088,"IsPrivate":false,"Code":"using Editor;\nusing Effigy;\nusing Sandbox;\nusing System;\nusing System.Collections.Generic;\n\nnamespace Marionette.EditorTools;\n\n/// \u003Csummary\u003E\n/// Brushing an existing material onto faces.\n///\n/// THE SAME GESTURE AS THE COLOUR BRUSH AND A DIFFERENT RESULT. The ring, the hover, the\n/// press-drag-release are all copied from \u003Csee cref=\u0022PaintFrame\u0022/\u003E deliberately: switching between\n/// the two should feel like changing what the brush is loaded with, not like learning a tool. What\n/// differs is what a dab does \u2014 colour blends into vertices here and now, a material dab is an\n/// edit to the feature history, so it has to go out to the window, change the document and come\n/// back as a rebuild.\n///\n/// WHICH IS WHY IT REPORTS RATHER THAN APPLIES. The viewport owns no studio and must not: it is\n/// handed a mesh to look at. So a dab raises \u003Csee cref=\u0022MaterialDabbed\u0022/\u003E with the faces it\n/// covered and the window does the assignment through MaterialDrop.Brush. That keeps the undo\n/// story ordinary \u2014 a material dab is the same kind of edit as dropping a material, and undoes the\n/// same way \u2014 instead of inventing a second undo path that only the brush knows about.\n///\n/// A DAB PER FRAME, AN UNDO STEP PER STROKE. The faces under the ring change as it moves, and each\n/// batch has to reach the document while the drag is still happening or the model would not follow\n/// the brush \u2014 so dabs are frequent. What a person expects to take back is the GESTURE, though, so\n/// undo is recorded once, at the press, by \u003Csee cref=\u0022MaterialStrokeStarted\u0022/\u003E. Recording it per\n/// dab instead would turn one sweep into fifty Ctrl\u002BZ presses.\n/// \u003C/summary\u003E\ninternal sealed partial class EffigyViewport\n{\n\tpublic MaterialBrushSession MaterialBrush { get; private set; }\n\n\tpublic bool IsMaterialBrushing =\u003E MaterialBrush is not null;\n\n\t/// \u003Csummary\u003ERaised with the faces one dab covered. The window assigns them and rebuilds; the\n\t/// list is reused between dabs, so it must be consumed rather than kept.\u003C/summary\u003E\n\tpublic Action\u003CIReadOnlyList\u003Cint\u003E\u003E MaterialDabbed { get; set; }\n\n\t/// \u003Csummary\u003E\n\t/// Raised once when a drag begins, BEFORE its first dab, so the window can record undo.\n\t///\n\t/// ONCE PER STROKE AND NOT PER DAB, which is the whole reason it is a separate signal. A dab\n\t/// fires every frame the brush moves, so recording undo there would make one sweep across a\n\t/// part into fifty Ctrl\u002BZ presses; recording it nowhere - which is what this did at first -\n\t/// leaves the stroke unundoable and sends Ctrl\u002BZ back past it to whatever came before. The\n\t/// gesture is the unit a person would expect to take back, so the gesture is what is recorded.\n\t/// \u003C/summary\u003E\n\tpublic Action MaterialStrokeStarted { get; set; }\n\n\t/// \u003Csummary\u003E\n\t/// Whether a material is actually chosen. The window sets it from the Materials browser.\n\t///\n\t/// THE RING HAS TO SAY SO. Without this the brush draws its ring, outlines the faces under it\n\t/// and then does nothing at all when dragged, because there is no material to lay down - which\n\t/// looks exactly like a broken tool rather than an unloaded one. Unloaded, the ring goes grey\n\t/// and stops outlining faces it is not going to take.\n\t/// \u003C/summary\u003E\n\tpublic bool MaterialBrushLoaded { get; set; }\n\n\tprivate MeshHit? _materialCursor;\n\tprivate bool _materialStroking;\n\n\tpublic void BeginMaterialBrush( MaterialBrushSession session )\n\t{\n\t\tMaterialBrush = session ?? throw new ArgumentNullException( nameof( session ) );\n\t\t_materialStroking = false;\n\t}\n\n\tpublic void EndMaterialBrush()\n\t{\n\t\tMaterialBrush = null;\n\t\t_materialCursor = null;\n\t\t_materialStroking = false;\n\t}\n\n\tprivate void MaterialBrushFrame()\n\t{\n\t\tif ( MaterialBrush is null )\n\t\t\treturn;\n\n\t\t_materialCursor = null;\n\n\t\t// Leaving the canvas does not end the drag, the same rule paint and sculpt keep: dragging\n\t\t// off the model and back on is ordinary.\n\t\tif ( _canvasHasCursor )\n\t\t{\n\t\t\tvar ray = Gizmo.CurrentRay;\n\t\t\tvar origin = new Vec3( ray.Position.x, ray.Position.y, ray.Position.z );\n\t\t\tvar direction = new Vec3( ray.Forward.x, ray.Forward.y, ray.Forward.z );\n\n\t\t\t_materialCursor = MaterialBrush.Hover( origin, direction );\n\n\t\t\t// Before the first dab, so the snapshot is the state the stroke is about to change.\n\t\t\tif ( Gizmo.WasLeftMousePressed \u0026\u0026 !_materialStroking )\n\t\t\t{\n\t\t\t\t_materialStroking = true;\n\t\t\t\tMaterialStrokeStarted?.Invoke();\n\t\t\t}\n\n\t\t\tif ( _materialStroking \u0026\u0026 Gizmo.IsLeftMouseDown \u0026\u0026 _materialCursor is { } hit )\n\t\t\t{\n\t\t\t\tvar faces = MaterialBrush.FacesAt( hit );\n\n\t\t\t\tif ( faces.Count \u003E 0 )\n\t\t\t\t\tMaterialDabbed?.Invoke( faces );\n\t\t\t}\n\t\t}\n\n\t\tif ( _materialStroking \u0026\u0026 !Gizmo.IsLeftMouseDown )\n\t\t\t_materialStroking = false;\n\n\t\tDrawMaterialCursor();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The ring, plus the faces it is about to take.\n\t///\n\t/// HIGHLIGHTING THE FACES IS THE WHOLE POINT HERE, in a way it is not for colour. A material dab\n\t/// covers whole faces, so what the ring encloses and what the dab takes are not the same shape \u2014\n\t/// on a coarse box a small ring still paints an entire side. Showing the faces means the tool\n\t/// says what it is about to do rather than letting the first click explain it.\n\t/// \u003C/summary\u003E\n\tprivate void DrawMaterialCursor()\n\t{\n\t\tif ( _materialCursor is not { } hit )\n\t\t\treturn;\n\n\t\tvar mesh = MaterialBrush.Mesh;\n\t\tvar colour = MaterialBrushLoaded ? MaterialCursorColor : UnloadedCursorColor;\n\n\t\tGizmo.Draw.IgnoreDepth = true;\n\t\tGizmo.Draw.Color = colour.WithAlpha( 0.5f );\n\t\tGizmo.Draw.LineThickness = 1.5f;\n\n\t\t// Only when there is something to lay down: outlining faces the brush will not touch is a\n\t\t// promise it cannot keep.\n\t\tforeach ( var faceIndex in MaterialBrushLoaded ? MaterialBrush.FacesAt( hit ) : NoFaces )\n\t\t{\n\t\t\tif ( faceIndex \u003C 0 || faceIndex \u003E= mesh.Faces.Count )\n\t\t\t\tcontinue;\n\n\t\t\tvar face = mesh.Faces[faceIndex];\n\n\t\t\tfor ( var c = 0; c \u003C face.Count; c\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar a = mesh.Positions[face.Indices[c]];\n\t\t\t\tvar b = mesh.Positions[face.Indices[(c \u002B 1) % face.Count]];\n\n\t\t\t\tGizmo.Draw.Line( new Vector3( a.x, a.y, a.z ), new Vector3( b.x, b.y, b.z ) );\n\t\t\t}\n\t\t}\n\n\t\tvar normal = new Vector3( hit.Normal.x, hit.Normal.y, hit.Normal.z ).Normal;\n\t\tvar centre = new Vector3( hit.Point.x, hit.Point.y, hit.Point.z );\n\n\t\tvar reference = MathF.Abs( normal.z ) \u003E 0.9f ? new Vector3( 1f, 0f, 0f ) : new Vector3( 0f, 0f, 1f );\n\t\tvar right = Vector3.Cross( normal, reference ).Normal;\n\t\tvar up = Vector3.Cross( normal, right ).Normal;\n\n\t\tvar radius = MaterialBrush.Radius;\n\n\t\tGizmo.Draw.Color = colour;\n\n\t\tvar lift = normal * (radius * 0.01f);\n\t\tconst int Segments = 40;\n\t\tvar previous = centre \u002B right * radius \u002B lift;\n\n\t\tfor ( var i = 1; i \u003C= Segments; i\u002B\u002B )\n\t\t{\n\t\t\tvar angle = i / (float)Segments * MathF.PI * 2f;\n\t\t\tvar point = centre \u002B (right * MathF.Cos( angle ) \u002B up * MathF.Sin( angle )) * radius \u002B lift;\n\n\t\t\tGizmo.Draw.Line( previous, point );\n\t\t\tprevious = point;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EAmber, so it is neither sculpt\u0027s blue nor paint\u0027s pink \u2014 the three brushes are told\n\t/// apart by the colour of their ring before anything else.\u003C/summary\u003E\n\tprivate static readonly Color MaterialCursorColor = new( 1f, 0.75f, 0.25f, 0.9f );\n\n\t/// \u003Csummary\u003EGrey, for a brush with nothing loaded - the ring still tracks the surface so the\n\t/// tool is visibly alive, it just does not claim it is about to paint anything.\u003C/summary\u003E\n\tprivate static readonly Color UnloadedCursorColor = new( 0.6f, 0.6f, 0.6f, 0.7f );\n\n\tprivate static readonly IReadOnlyList\u003Cint\u003E NoFaces = new List\u003Cint\u003E();\n}\n"},{"Ident":"pooh.geppetto","Path":"Editor/EffigyEditor/EffigyViewport.Selection.cs","FileName":"EffigyViewport.Selection.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":411088,"IsPrivate":false,"Code":"using Editor;\nusing Effigy;\nusing Sandbox;\nusing System;\nusing System.Collections.Generic;\n\nnamespace Marionette.EditorTools;\n\n/// \u003Csummary\u003E\n/// Idle geometry selection \u2014 click a face or a part, then use a tool on it.\n///\n/// Separate from FacePickMode / BodyPickMode. Those are a dialog asking a question; this is the\n/// selection that exists when nobody is asking. Onshape works the same way: you point at a face,\n/// it lights up, you click it, then Draft/Hole/Sketch consume what you already picked instead of\n/// making you pick it again inside a dialog.\n/// \u003C/summary\u003E\ninternal sealed partial class EffigyViewport\n{\n\t/// \u003Csummary\u003EFaces currently selected while idle. Empty when the last click was a whole part\n\t/// in the Parts list, or nothing.\u003C/summary\u003E\n\tpublic IReadOnlyList\u003CFaceRef\u003E IdleFaces =\u003E _idleFaces;\n\n\t/// \u003Csummary\u003EEdges currently selected while idle. Empty unless the last click landed near a\n\t/// rim rather than in the middle of a face.\u003C/summary\u003E\n\tpublic IReadOnlyList\u003CEdgeRef\u003E IdleEdges =\u003E _idleEdges;\n\n\t/// \u003Csummary\u003EBodies currently selected while idle. A face click names its owning body so the\n\t/// Parts list can highlight the same row; a Parts-list click names the body and no faces.\n\t/// \u003C/summary\u003E\n\tpublic IReadOnlyList\u003Cstring\u003E IdleBodyIds =\u003E _idleBodyIds;\n\n\t/// \u003Csummary\u003ECommitted sketch currently selected while idle, or null. Extrude/Revolve read\n\t/// this instead of waiting for a second click.\u003C/summary\u003E\n\tpublic string IdleSketchFeatureId =\u003E _idleSketchFeatureId;\n\n\t/// \u003Csummary\u003ERegion seeds on \u003Csee cref=\u0022IdleSketchFeatureId\u0022/\u003E. Empty means the whole sketch.\n\t/// \u003C/summary\u003E\n\tpublic IReadOnlyList\u003CVec2\u003E IdleRegionSeeds =\u003E _idleRegionSeeds;\n\n\tpublic bool HasIdleSelection =\u003E\n\t\t_idleFaces.Count \u003E 0 || _idleEdges.Count \u003E 0 || _idleBodyIds.Count \u003E 0\n\t\t|| _idleSketchFeatureId is not null;\n\n\t/// \u003Csummary\u003ERaised after a click in the viewport changes the idle selection, so the Parts\n\t/// list can stay in step. Not raised when the list itself drove the change.\u003C/summary\u003E\n\tpublic Action IdleSelectionChanged { get; set; }\n\n\tprivate readonly List\u003CFaceRef\u003E _idleFaces = new();\n\tprivate readonly List\u003CEdgeRef\u003E _idleEdges = new();\n\tprivate readonly List\u003Cstring\u003E _idleBodyIds = new();\n\tprivate string _idleSketchFeatureId;\n\tprivate readonly List\u003CVec2\u003E _idleRegionSeeds = new();\n\n\t/// \u003Csummary\u003EHow close a click must be to a rim, in screen pixels, before it counts as an\n\t/// edge rather than the face. Wide enough to grab, tight enough that a click in the middle\n\t/// of a face stays a face.\u003C/summary\u003E\n\tprivate const float EdgePickPixels = 10f;\n\n\t/// \u003Csummary\u003EWhether a left click on a face is a selection rather than an answer to a dialog.\n\t/// Anything else with a click of its own owns the mouse while it is armed.\u003C/summary\u003E\n\tprivate bool IdlePickingAllowed =\u003E\n\t\t!IsSketching \u0026\u0026 !IsSplineEditing \u0026\u0026 !IsSculpting \u0026\u0026 !IsMeshEditing \u0026\u0026 !IsPainting \u0026\u0026 !IsMaterialBrushing \u0026\u0026 !IsNoting \u0026\u0026 !IsWeightPainting\n\t\t\u0026\u0026 !PlanePickMode \u0026\u0026 !SketchPickMode \u0026\u0026 !FacePickMode \u0026\u0026 !EdgePickMode \u0026\u0026 !BodyPickMode\n\t\t\u0026\u0026 !BoneToolActive\n\t\t\u0026\u0026 !_draggingOrigin \u0026\u0026 !_draggingLight \u0026\u0026 !_draggingFace;\n\n\t/// \u003Csummary\u003E\n\t/// Replace the idle selection with whole bodies, clearing any face picks.\n\t///\n\t/// This is what the Parts list does: clicking a row selects that part, not one of its faces.\n\t/// Fillet, shell, transform then act on the part; Sketch/Draft still need a face, so they\n\t/// open asking for one.\n\t/// \u003C/summary\u003E\n\tpublic void SelectBodies( IReadOnlyList\u003Cstring\u003E bodyIds )\n\t{\n\t\t_idleFaces.Clear();\n\t\t_idleEdges.Clear();\n\t\t_idleBodyIds.Clear();\n\t\tClearIdleSketch();\n\n\t\tif ( bodyIds is not null )\n\t\t{\n\t\t\tforeach ( var id in bodyIds )\n\t\t\t{\n\t\t\t\tif ( !string.IsNullOrEmpty( id ) \u0026\u0026 !_idleBodyIds.Contains( id ) )\n\t\t\t\t\t_idleBodyIds.Add( id );\n\t\t\t}\n\t\t}\n\n\t\tIdleSelectionChanged?.Invoke();\n\t}\n\n\tpublic void ClearIdleSelection()\n\t{\n\t\tif ( !HasIdleSelection )\n\t\t\treturn;\n\n\t\t_idleFaces.Clear();\n\t\t_idleEdges.Clear();\n\t\t_idleBodyIds.Clear();\n\t\tClearIdleSketch();\n\t\tIdleSelectionChanged?.Invoke();\n\t}\n\n\tvoid ClearIdleSketch()\n\t{\n\t\t_idleSketchFeatureId = null;\n\t\t_idleRegionSeeds.Clear();\n\t}\n\n\t/// \u003Csummary\u003ESelect a committed sketch, optionally a single closed region of it.\u003C/summary\u003E\n\tpublic void SelectIdleSketch( string featureId, Vec2? seed, bool add = false )\n\t{\n\t\tif ( string.IsNullOrEmpty( featureId ) )\n\t\t{\n\t\t\tif ( _idleSketchFeatureId is null )\n\t\t\t\treturn;\n\n\t\t\tClearIdleSketch();\n\t\t\tIdleSelectionChanged?.Invoke();\n\t\t\treturn;\n\t\t}\n\n\t\tif ( add \u0026\u0026 _idleSketchFeatureId == featureId \u0026\u0026 seed is { } extra )\n\t\t{\n\t\t\tfor ( var i = 0; i \u003C _idleRegionSeeds.Count; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar existing = _idleRegionSeeds[i];\n\n\t\t\t\tif ( MathF.Abs( existing.x - extra.x ) \u003E 1e-4f || MathF.Abs( existing.y - extra.y ) \u003E 1e-4f )\n\t\t\t\t\tcontinue;\n\n\t\t\t\t_idleRegionSeeds.RemoveAt( i );\n\n\t\t\t\tif ( _idleRegionSeeds.Count == 0 )\n\t\t\t\t\t_idleSketchFeatureId = null;\n\n\t\t\t\tIdleSelectionChanged?.Invoke();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t_idleRegionSeeds.Add( extra );\n\t\t\tIdleSelectionChanged?.Invoke();\n\t\t\treturn;\n\t\t}\n\n\t\t_idleFaces.Clear();\n\t\t_idleEdges.Clear();\n\t\t_idleBodyIds.Clear();\n\t\t_idleSketchFeatureId = featureId;\n\t\t_idleRegionSeeds.Clear();\n\n\t\tif ( seed is { } one )\n\t\t\t_idleRegionSeeds.Add( one );\n\n\t\tIdleSelectionChanged?.Invoke();\n\t}\n\n\t/// \u003Csummary\u003EDrop faces and parts whose bodies no longer exist after a rebuild. Called from\n\t/// SetDisplayBodies so a deleted part cannot stay selected against nothing.\u003C/summary\u003E\n\tprivate void PruneIdleSelection()\n\t{\n\t\tif ( !HasIdleSelection )\n\t\t\treturn;\n\n\t\tvar ids = new HashSet\u003Cstring\u003E();\n\n\t\tforeach ( var body in _displayBodies )\n\t\t{\n\t\t\tif ( body?.Id is { } id )\n\t\t\t\tids.Add( id );\n\t\t}\n\n\t\tvar changed = _idleFaces.RemoveAll( f =\u003E !ids.Contains( f.BodyId ) ) \u003E 0\n\t\t\t| _idleEdges.RemoveAll( e =\u003E !ids.Contains( e.BodyId ) ) \u003E 0\n\t\t\t| _idleBodyIds.RemoveAll( id =\u003E !ids.Contains( id ) ) \u003E 0;\n\n\t\tif ( _idleSketchFeatureId is not null )\n\t\t{\n\t\t\tvar found = false;\n\n\t\t\tforeach ( var pickable in _pickableSketches )\n\t\t\t{\n\t\t\t\tif ( pickable.FeatureId == _idleSketchFeatureId )\n\t\t\t\t{\n\t\t\t\t\tfound = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( !found )\n\t\t\t{\n\t\t\t\tClearIdleSketch();\n\t\t\t\tchanged = true;\n\t\t\t}\n\t\t}\n\n\t\tif ( changed )\n\t\t\tIdleSelectionChanged?.Invoke();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Hover the face under the cursor, keep already-picked faces and parts lit, and take a click\n\t/// as a selection when nothing else owns the mouse.\n\t///\n\t/// Runs after the origin handle so a click on the origin is not also a click on the face that\n\t/// happens to sit behind it. Gizmo.HasHovered covers the origin, the plane-corner handles and\n\t/// anything else that registered a hitbox this frame.\n\t/// \u003C/summary\u003E\n\tprivate void IdleSelectionFrame()\n\t{\n\t\tDrawIdleSelection();\n\n\t\tif ( !IdlePickingAllowed )\n\t\t\treturn;\n\n\t\tif ( !_canvasHasCursor )\n\t\t\treturn;\n\n\t\t// A bone under the cursor is a bone click, even when the mesh is in front of it.\n\t\t// Taking the part instead is why clicking a 3D bone did nothing and Assign stayed grey.\n\t\tif ( RigMode \u0026\u0026 TryPickBoneUnderCursor( out _ ) )\n\t\t\treturn;\n\n\t\t// Sketches are a CAD question. In the rig workspace a click names a part (or a bone),\n\t\t// and a profile lighting up under the cursor would steal the body you meant to pin.\n\t\tif ( !RigMode\n\t\t\t\u0026\u0026 TryResolveSketchHover( out var sketchId, out var sketchSeed, out var sketchDistance )\n\t\t\t\u0026\u0026 SketchBeatsFace( sketchDistance ) )\n\t\t{\n\t\t\t_hoveredFaceBodyId = sketchId;\n\t\t\tDrawSketchPickHighlight( sketchId, sketchSeed );\n\n\t\t\tif ( Gizmo.WasLeftMousePressed \u0026\u0026 !Gizmo.HasHovered )\n\t\t\t{\n\t\t\t\tif ( OriginSelected )\n\t\t\t\t\tDeselectOrigin();\n\n\t\t\t\tSelectIdleSketch( sketchId, sketchSeed, Gizmo.IsShiftPressed );\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\tif ( TryPickFaceUnderCursor( out var hit ) )\n\t\t{\n\t\t\t_hoveredFaceBodyId = hit.Body.Id;\n\n\t\t\t// Rigging assigns WHOLE parts. A face or an edge under the cursor is still the part,\n\t\t\t// and lighting a single face would look like CAD selection in a workspace that has\n\t\t\t// no face tools.\n\t\t\tif ( RigMode )\n\t\t\t{\n\t\t\t\tDrawBodyHighlight( hit.Body, BodyPickHoverColor );\n\n\t\t\t\tif ( Gizmo.WasLeftMousePressed \u0026\u0026 !Gizmo.HasHovered )\n\t\t\t\t\tSelectIdleBody( hit.Body.Id, Gizmo.IsShiftPressed );\n\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( TryIdleEdge( hit, out var edge, out var key ) )\n\t\t\t{\n\t\t\t\tDrawEdge( hit.Body, key, FaceHighlightColor );\n\n\t\t\t\tif ( Gizmo.WasLeftMousePressed \u0026\u0026 !Gizmo.HasHovered )\n\t\t\t\t{\n\t\t\t\t\tif ( OriginSelected )\n\t\t\t\t\t\tDeselectOrigin();\n\n\t\t\t\t\tSelectIdleEdge( hit.Body, edge, Gizmo.IsShiftPressed );\n\t\t\t\t}\n\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tDrawHoveredFace( hit.Body, hit.FaceIndex );\n\n\t\t\tif ( Gizmo.WasLeftMousePressed \u0026\u0026 !Gizmo.HasHovered )\n\t\t\t{\n\t\t\t\tif ( OriginSelected )\n\t\t\t\t\tDeselectOrigin();\n\n\t\t\t\tSelectIdleFace( hit, Gizmo.IsShiftPressed );\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\tif ( Gizmo.WasLeftMousePressed \u0026\u0026 !Gizmo.HasHovered \u0026\u0026 !Gizmo.IsHovered )\n\t\t\tClearIdleSelection();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Chosen idle geometry, in the same amber the dialog uses for a committed pick.\n\t///\n\t/// Skipped when a dialog is already drawing its own SelectedFaces / SelectedBodyIds, so the\n\t/// two never paint the same face twice in slightly different states.\n\t/// \u003C/summary\u003E\n\t/// \u003Csummary\u003EA sketch on a face sits at the same depth as that face. Prefer the sketch when\n\t/// they tie; prefer a solid that is genuinely in front of the sketch plane.\u003C/summary\u003E\n\tprivate bool SketchBeatsFace( float sketchDistance )\n\t{\n\t\tif ( !TryPickFaceUnderCursor( out var hit ) )\n\t\t\treturn true;\n\n\t\tvar faceDistance = (hit.Reference.Point - _cursorRayOrigin).Length;\n\n\t\treturn sketchDistance \u003C= faceDistance \u002B 0.05f;\n\t}\n\n\tprivate void DrawIdleSelection()\n\t{\n\t\t// A BRUSH WANTS THE SURFACE, NOT THE SELECTION. An amber wash and wireframe over the part\n\t\t// you are painting hides the very colour you are laying down. The selection survives \u2014 it\n\t\t// comes back when the brush is put away \u2014 only the drawing of it stops. The shell is cleared\n\t\t// too: it is a cached scene object, and a frame that simply stops drawing leaves it lit.\n\t\tif ( IsPainting || IsSculpting || IsMeshEditing || IsMaterialBrushing || IsWeightPainting )\n\t\t{\n\t\t\tSyncBodyHighlight( null, BodySelectedColor );\n\t\t\treturn;\n\t\t}\n\n\t\tif ( (SelectedSketchFeatureId is null || SelectedSketchFeatureId.Length == 0)\n\t\t\t\u0026\u0026 _idleSketchFeatureId is not null )\n\t\t{\n\t\t\tvar seed = _idleRegionSeeds.Count == 1 ? _idleRegionSeeds[0] : (Vec2?)null;\n\n\t\t\tif ( _idleRegionSeeds.Count \u003C= 1 )\n\t\t\t\tDrawSketchPickHighlight( _idleSketchFeatureId, seed );\n\t\t\telse\n\t\t\t{\n\t\t\t\tforeach ( var region in _idleRegionSeeds )\n\t\t\t\t\tDrawSketchPickHighlight( _idleSketchFeatureId, region );\n\t\t\t}\n\t\t}\n\n\t\tvar drawFaces = SelectedFaces is null || SelectedFaces.Count == 0;\n\t\tvar drawEdges = SelectedEdges is null || SelectedEdges.Count == 0;\n\t\tvar drawBodies = SelectedBodyIds is null || SelectedBodyIds.Count == 0;\n\n\t\tif ( drawEdges \u0026\u0026 _idleEdges.Count \u003E 0 )\n\t\t{\n\t\t\tforeach ( var edge in _idleEdges )\n\t\t\t{\n\t\t\t\tif ( FacePlane.TryResolveEdge( _displayBodies, edge, out var body, out var key ) )\n\t\t\t\t\tDrawEdge( body, key, FaceSelectedColor );\n\t\t\t}\n\t\t}\n\n\t\tif ( drawFaces \u0026\u0026 _idleFaces.Count \u003E 0 )\n\t\t{\n\t\t\tforeach ( var face in _idleFaces )\n\t\t\t{\n\t\t\t\tif ( FacePlane.TryResolveFace( _displayBodies, face, out var body, out var index ) )\n\t\t\t\t\tDrawFace( body, index, FaceSelectedColor );\n\t\t\t}\n\n\t\t\t// A face pick names its owning body, so _idleBodyIds is not empty here \u2014 but the thing\n\t\t\t// selected is the face, and shelling the whole part around it would bury the pick.\n\t\t\tSyncBodyHighlight( null, BodySelectedColor );\n\t\t\treturn;\n\t\t}\n\n\t\t// A DIALOG OWNS THE SHELL WHILE ONE IS PICKING BODIES. DrawChosenBodies drives the same\n\t\t// cache from the other side, and clearing it here \u2014 which is what \u0022no idle selection\u0022 would\n\t\t// otherwise mean \u2014 would blank the picker\u0027s own highlight every frame.\n\t\tif ( !drawBodies )\n\t\t\treturn;\n\n\t\t// EVERY OTHER PATH OUT OF HERE ANSWERS THIS, including the ones with nothing to light. The\n\t\t// shell is a cached scene object rather than lines drawn this frame (see\n\t\t// EffigyViewport.Highlight), so a frame that simply stops drawing it leaves it on screen.\n\t\t_highlightBodies.Clear();\n\n\t\tforeach ( var body in _displayBodies )\n\t\t{\n\t\t\tif ( body?.Id is not { } id || !_idleBodyIds.Contains( id ) )\n\t\t\t\tcontinue;\n\n\t\t\t// THE SAME DRAW A VIEWPORT CLICK USES, face for face.\n\t\t\t//\n\t\t\t// Not a similar one, not a body-shaped variant of one \u2014 DrawFace with FaceSelectedColor,\n\t\t\t// which is literally the call that lights a face up when you click the solid. Clicking a\n\t\t\t// row in the Parts list means \u0022that whole part\u0022, so it runs over every face of the body\n\t\t\t// and the part comes up in the amber you already know. Every attempt at this that drew\n\t\t\t// something of its own instead \u2014 the wash-and-outline body highlight, the cached shell \u2014\n\t\t\t// failed in a way that looked from the outside like the click doing nothing at all.\n\t\t\t//\n\t\t\t// The shell stays for the case that earns it: an import too dense to draw a face at a\n\t\t\t// time, where this loop is hundreds of thousands of gizmo calls a frame.\n\t\t\t// ONE PASS, NOT ONE PER FACE. Calling DrawFace in a loop was the obvious way to say \u0022the\n\t\t\t// same amber a face pick gives you\u0022 and it is quadratic: DrawFace resolves the COPLANAR\n\t\t\t// SURFACE the face belongs to and draws every face in it, so a surface of forty faces is\n\t\t\t// drawn forty times, and a part with a few thousand faces spends the frame redrawing\n\t\t\t// itself. DrawBodyHighlight paints each face exactly once in the colour it is handed,\n\t\t\t// which is the same picture for a fraction of the work.\n\t\t\tif ( (body.Mesh?.FaceCount ?? 0) \u003C= HoverHighlightFaceBudget )\n\t\t\t\tDrawBodyHighlight( body, FaceSelectedColor );\n\t\t\telse\n\t\t\t\t_highlightBodies.Add( body );\n\t\t}\n\n\t\tSyncBodyHighlight( _highlightBodies, BodySelectedColor );\n\t}\n\n\t/// \u003Csummary\u003EScratch for the line above \u2014 reused rather than allocated, since it is rebuilt every\n\t/// frame and thrown away on almost all of them.\u003C/summary\u003E\n\tprivate readonly List\u003CBody\u003E _highlightBodies = new();\n\n\t/// \u003Csummary\u003E\n\t/// Select one face outright, from something other than a click \u2014 the right-click menu, which\n\t/// has already resolved the face under the cursor and now wants a tool pointed at it.\n\t///\n\t/// Replaces the selection rather than adding to it: the face you just right-clicked is the face\n\t/// you meant, and quietly bundling it with whatever was lit a moment ago would hand the tool\n\t/// more than you asked for.\n\t/// \u003C/summary\u003E\n\tpublic void SelectFace( EffigyFaceHit hit ) =\u003E SelectIdleFace( hit, add: false );\n\n\t/// \u003Csummary\u003EClick a part in the rig workspace: the whole body, never one of its faces.\u003C/summary\u003E\n\tprivate void SelectIdleBody( string bodyId, bool add )\n\t{\n\t\tif ( string.IsNullOrEmpty( bodyId ) )\n\t\t\treturn;\n\n\t\t_idleFaces.Clear();\n\t\t_idleEdges.Clear();\n\t\tClearIdleSketch();\n\n\t\tif ( add )\n\t\t{\n\t\t\tif ( !_idleBodyIds.Remove( bodyId ) )\n\t\t\t\t_idleBodyIds.Add( bodyId );\n\t\t}\n\t\telse\n\t\t{\n\t\t\t_idleBodyIds.Clear();\n\t\t\t_idleBodyIds.Add( bodyId );\n\t\t}\n\n\t\tIdleSelectionChanged?.Invoke();\n\t}\n\n\t/// \u003Csummary\u003EClick a face: replace the selection, or Shift-click to toggle it in.\u003C/summary\u003E\n\tprivate void SelectIdleFace( EffigyFaceHit hit, bool add )\n\t{\n\t\tvar face = hit.Reference;\n\n\t\tif ( add )\n\t\t{\n\t\t\tfor ( var i = 0; i \u003C _idleFaces.Count; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tif ( !SameIdleFace( _idleFaces[i], face ) )\n\t\t\t\t\tcontinue;\n\n\t\t\t\t_idleFaces.RemoveAt( i );\n\t\t\t\tRebuildIdleBodyIdsFromFaces();\n\t\t\t\tIdleSelectionChanged?.Invoke();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t_idleEdges.Clear();\n\t\t\tClearIdleSketch();\n\t\t\t_idleFaces.Add( face );\n\n\t\t\tif ( !_idleBodyIds.Contains( hit.Body.Id ) )\n\t\t\t\t_idleBodyIds.Add( hit.Body.Id );\n\t\t}\n\t\telse\n\t\t{\n\t\t\t_idleFaces.Clear();\n\t\t\t_idleEdges.Clear();\n\t\t\t_idleBodyIds.Clear();\n\t\t\tClearIdleSketch();\n\t\t\t_idleFaces.Add( face );\n\t\t\t_idleBodyIds.Add( hit.Body.Id );\n\t\t}\n\n\t\tIdleSelectionChanged?.Invoke();\n\t}\n\n\tprivate void SelectIdleEdge( Body body, EdgeRef edge, bool add )\n\t{\n\t\tif ( add )\n\t\t{\n\t\t\tfor ( var i = 0; i \u003C _idleEdges.Count; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tif ( !SameIdleEdge( _idleEdges[i], edge ) )\n\t\t\t\t\tcontinue;\n\n\t\t\t\t_idleEdges.RemoveAt( i );\n\t\t\t\tRebuildIdleBodyIdsFromFaces();\n\t\t\t\tIdleSelectionChanged?.Invoke();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t_idleFaces.Clear();\n\t\t\tClearIdleSketch();\n\t\t\t_idleEdges.Add( edge );\n\n\t\t\tif ( !_idleBodyIds.Contains( body.Id ) )\n\t\t\t\t_idleBodyIds.Add( body.Id );\n\t\t}\n\t\telse\n\t\t{\n\t\t\t_idleFaces.Clear();\n\t\t\t_idleEdges.Clear();\n\t\t\t_idleBodyIds.Clear();\n\t\t\tClearIdleSketch();\n\t\t\t_idleEdges.Add( edge );\n\t\t\t_idleBodyIds.Add( body.Id );\n\t\t}\n\n\t\tIdleSelectionChanged?.Invoke();\n\t}\n\n\tprivate bool TryIdleEdge( EffigyFaceHit hit, out EdgeRef edge, out EdgeKey key )\n\t{\n\t\tedge = default;\n\t\tkey = default;\n\n\t\tif ( !TryClosestEdge( hit.Body.Mesh, hit.FaceIndex, hit.Reference.Point, out key, out var distance ) )\n\t\t\treturn false;\n\n\t\tvar point = hit.Reference.Point;\n\t\tvar threshold = WorldRadiusAt( new Vector3( point.x, point.y, point.z ), EdgePickPixels );\n\n\t\tif ( distance \u003E threshold )\n\t\t\treturn false;\n\n\t\tedge = FacePlane.Capture( hit.Body, key );\n\t\treturn true;\n\t}\n\n\tprivate void RebuildIdleBodyIdsFromFaces()\n\t{\n\t\t_idleBodyIds.Clear();\n\n\t\tforeach ( var face in _idleFaces )\n\t\t{\n\t\t\tif ( !_idleBodyIds.Contains( face.BodyId ) )\n\t\t\t\t_idleBodyIds.Add( face.BodyId );\n\t\t}\n\n\t\tforeach ( var edge in _idleEdges )\n\t\t{\n\t\t\tif ( !_idleBodyIds.Contains( edge.BodyId ) )\n\t\t\t\t_idleBodyIds.Add( edge.BodyId );\n\t\t}\n\t}\n\n\tprivate bool SameIdleEdge( EdgeRef a, EdgeRef b )\n\t{\n\t\tif ( !FacePlane.TryResolveEdge( _displayBodies, a, out var bodyA, out var keyA )\n\t\t\t|| !FacePlane.TryResolveEdge( _displayBodies, b, out var bodyB, out var keyB ) )\n\t\t\treturn false;\n\n\t\treturn bodyA.Id == bodyB.Id \u0026\u0026 keyA.Equals( keyB );\n\t}\n\n\t/// \u003Csummary\u003EMatch by resolved face, not by stored FaceRef equality \u2014 two clicks on the same\n\t/// face produce two references with slightly different hit points.\u003C/summary\u003E\n\tprivate bool SameIdleFace( FaceRef a, FaceRef b )\n\t{\n\t\tif ( !FacePlane.TryResolveFace( _displayBodies, a, out var bodyA, out var indexA )\n\t\t\t|| !FacePlane.TryResolveFace( _displayBodies, b, out var bodyB, out var indexB ) )\n\t\t\treturn false;\n\n\t\treturn bodyA.Id == bodyB.Id \u0026\u0026 indexA == indexB;\n\t}\n}\n"},{"Ident":"pooh.geppetto","Path":"Editor/EffigyEditor/EffigyViewport.Splines.cs","FileName":"EffigyViewport.Splines.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":411088,"IsPrivate":false,"Code":"using Editor;\nusing Effigy;\nusing Sandbox;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace Marionette.EditorTools;\n\n/// \u003Csummary\u003E\n/// Drawing a spline: click to drop points, drag a point to move it, and the curve follows.\n///\n/// A CLICK LANDS ON THE MODEL WHEN THERE IS ONE UNDER IT, and on a plane through the last point\n/// facing the camera otherwise \u2014 so a cable can be clicked straight onto a body\u0027s surface, and\n/// the run between two bodies is clicked out in the air at the height you were already at. The\n/// first point with nothing under it lands on the ground plane, which is where a path usually\n/// starts. A drag moves the point in the plane facing the camera through it: the point goes\n/// where the mouse goes, and depth is left alone.\n///\n/// The feature owns the points; this only edits them and says so. The curve drawn here is\n/// sampled from the feature directly, so it follows the mouse without waiting on a rebuild,\n/// and the rebuild that follows an edit is what turns it into the tube.\n/// \u003C/summary\u003E\ninternal sealed partial class EffigyViewport\n{\n\tpublic SplineFeature SplineFeature { get; private set; }\n\tpublic bool IsSplineEditing =\u003E SplineFeature is not null;\n\n\t/// \u003Csummary\u003EA point was added, moved or removed. The window marks the feature dirty and rebuilds.\u003C/summary\u003E\n\tpublic Action SplineEdited { get; set; }\n\n\t/// \u003Csummary\u003EEnter, or a double-click: the drawing is done.\u003C/summary\u003E\n\tpublic Action SplineFinishRequested { get; set; }\n\n\tprivate int _splineHover = -1;\n\tprivate int _splineDrag = -1;\n\tprivate bool _splineDragMoved;\n\tprivate Vector3 _splineDragNormal;\n\tprivate Vec3 _splineDragOrigin;\n\n\tprivate static readonly Color SplineColor = new( 0.94f, 0.63f, 0.23f );\n\tprivate static readonly Color SplinePointColor = new( 0.96f, 0.96f, 0.98f );\n\tprivate static readonly Color SplineHoverColor = new( 0.36f, 0.58f, 1f );\n\n\tpublic void BeginSplineEdit( SplineFeature feature )\n\t{\n\t\tSplineFeature = feature;\n\t\t_splineHover = -1;\n\t\t_splineDrag = -1;\n\t\tUpdate();\n\t}\n\n\tpublic void EndSplineEdit()\n\t{\n\t\tSplineFeature = null;\n\t\t_splineHover = -1;\n\t\t_splineDrag = -1;\n\t\tUpdate();\n\t}\n\n\t/// \u003Csummary\u003ERemove the last point \u2014 Backspace while drawing.\u003C/summary\u003E\n\tpublic bool RemoveLastSplinePoint()\n\t{\n\t\tif ( SplineFeature is not { } feature || feature.Points.Count == 0 )\n\t\t\treturn false;\n\n\t\tfeature.Points.RemoveAt( feature.Points.Count - 1 );\n\t\t_splineHover = -1;\n\t\tSplineEdited?.Invoke();\n\t\treturn true;\n\t}\n\n\tprivate void SplineFrame()\n\t{\n\t\tif ( SplineFeature is not { } feature )\n\t\t\treturn;\n\n\t\tvar points = feature.Points;\n\t\tvar ray = Gizmo.CurrentRay;\n\t\tvar origin = new Vec3( ray.Position.x, ray.Position.y, ray.Position.z );\n\t\tvar direction = new Vec3( ray.Forward.x, ray.Forward.y, ray.Forward.z );\n\t\tvar cameraForward = Gizmo.CameraTransform.Rotation.Forward;\n\n\t\t// --- the drag, first, so a held button keeps its point even off the handle ---\n\t\tif ( _splineDrag \u003E= 0 )\n\t\t{\n\t\t\tif ( Gizmo.IsLeftMouseDown \u0026\u0026 _splineDrag \u003C points.Count )\n\t\t\t{\n\t\t\t\tif ( RayPlane( origin, direction, _splineDragOrigin, new Vec3( _splineDragNormal.x, _splineDragNormal.y, _splineDragNormal.z ), out var moved ) )\n\t\t\t\t{\n\t\t\t\t\tif ( (moved - points[_splineDrag]).LengthSquared \u003E 1e-10f )\n\t\t\t\t\t{\n\t\t\t\t\t\tpoints[_splineDrag] = moved;\n\t\t\t\t\t\t_splineDragMoved = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tvar moved = _splineDragMoved;\n\t\t\t\t_splineDrag = -1;\n\t\t\t\t_splineDragMoved = false;\n\n\t\t\t\tif ( moved )\n\t\t\t\t\tSplineEdited?.Invoke();\n\t\t\t}\n\t\t}\n\n\t\t// --- hover: the nearest point within a handle\u0027s width of the cursor ray ---\n\t\t_splineHover = -1;\n\n\t\tif ( _canvasHasCursor \u0026\u0026 _splineDrag \u003C 0 )\n\t\t{\n\t\t\tvar best = float.MaxValue;\n\n\t\t\tfor ( var i = 0; i \u003C points.Count; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar toPoint = points[i] - origin;\n\t\t\t\tvar along = Vec3.Dot( toPoint, direction );\n\n\t\t\t\tif ( along \u003C= 0f )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tvar perpendicular = (toPoint - direction * along).Length;\n\t\t\t\tvar handle = HandleRadius( along ) * 1.6f;\n\n\t\t\t\tif ( perpendicular \u003C handle \u0026\u0026 along \u003C best )\n\t\t\t\t{\n\t\t\t\t\tbest = along;\n\t\t\t\t\t_splineHover = i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// --- where a click would land ---\n\t\tVec3? landing = null;\n\n\t\tif ( _canvasHasCursor \u0026\u0026 _splineHover \u003C 0 \u0026\u0026 _splineDrag \u003C 0 )\n\t\t\tlanding = SplineLanding( origin, direction, points, cameraForward );\n\n\t\t// --- input ---\n\t\tif ( _canvasHasCursor \u0026\u0026 Gizmo.WasLeftMousePressed \u0026\u0026 !Gizmo.HasHovered \u0026\u0026 _splineDrag \u003C 0 )\n\t\t{\n\t\t\tif ( _splineHover \u003E= 0 )\n\t\t\t{\n\t\t\t\t_splineDrag = _splineHover;\n\t\t\t\t_splineDragMoved = false;\n\t\t\t\t_splineDragNormal = cameraForward;\n\t\t\t\t_splineDragOrigin = points[_splineHover];\n\t\t\t}\n\t\t\telse if ( landing is { } at )\n\t\t\t{\n\t\t\t\tpoints.Add( at );\n\t\t\t\tSplineEdited?.Invoke();\n\t\t\t}\n\t\t}\n\n\t\t// --- drawing ---\n\t\tvar spline = feature.ToSpline();\n\t\tvar path = spline.Sample( 10 );\n\n\t\tGizmo.Draw.IgnoreDepth = true;\n\t\tGizmo.Draw.Color = SplineColor;\n\t\tGizmo.Draw.LineThickness = 2.5f;\n\n\t\tfor ( var i = 0; i \u002B 1 \u003C path.Count; i\u002B\u002B )\n\t\t\tGizmo.Draw.Line( ToWorldDir( path[i] ), ToWorldDir( path[i \u002B 1] ) );\n\n\t\tif ( spline.Closed \u0026\u0026 path.Count \u003E 2 )\n\t\t\tGizmo.Draw.Line( ToWorldDir( path[^1] ), ToWorldDir( path[0] ) );\n\n\t\t// The straight polyline between control points, faint, so the curve\u0027s relationship to\n\t\t// the clicks stays readable on a tight bend.\n\t\tif ( spline.Smooth \u0026\u0026 points.Count \u003E 2 )\n\t\t{\n\t\t\tGizmo.Draw.Color = SplineColor.WithAlpha( 0.25f );\n\t\t\tGizmo.Draw.LineThickness = 1f;\n\n\t\t\tfor ( var i = 0; i \u002B 1 \u003C points.Count; i\u002B\u002B )\n\t\t\t\tGizmo.Draw.Line( ToWorldDir( points[i] ), ToWorldDir( points[i \u002B 1] ) );\n\t\t}\n\n\t\tfor ( var i = 0; i \u003C points.Count; i\u002B\u002B )\n\t\t{\n\t\t\tvar world = ToWorldDir( points[i] );\n\t\t\tvar distance = (points[i] - origin).Length;\n\t\t\tvar radius = HandleRadius( distance );\n\t\t\tvar lit = i == _splineHover || i == _splineDrag;\n\n\t\t\tGizmo.Draw.Color = lit ? SplineHoverColor : i == points.Count - 1 ? SplineColor : SplinePointColor;\n\t\t\tGizmo.Draw.SolidSphere( world, lit ? radius * 1.3f : radius, 8, 8 );\n\t\t}\n\n\t\tif ( landing is { } ghost )\n\t\t{\n\t\t\tvar world = ToWorldDir( ghost );\n\t\t\tvar radius = HandleRadius( (ghost - origin).Length );\n\n\t\t\tGizmo.Draw.Color = SplineColor.WithAlpha( 0.55f );\n\t\t\tGizmo.Draw.LineSphere( new Sphere( world, radius ), 8 );\n\n\t\t\tif ( points.Count \u003E 0 )\n\t\t\t{\n\t\t\t\tGizmo.Draw.Color = SplineColor.WithAlpha( 0.35f );\n\t\t\t\tGizmo.Draw.LineThickness = 1f;\n\t\t\t\tGizmo.Draw.Line( ToWorldDir( points[^1] ), world );\n\t\t\t}\n\t\t}\n\n\t\tGizmo.Draw.LineThickness = 1f;\n\t\tGizmo.Draw.IgnoreDepth = false;\n\t}\n\n\t/// \u003Csummary\u003EA handle that stays the same size on screen: a fraction of its distance from the eye.\u003C/summary\u003E\n\tprivate static float HandleRadius( float distance ) =\u003E MathF.Max( distance * 0.008f, 0.02f );\n\n\t/// \u003Csummary\u003E\n\t/// Where a click lands: the model\u0027s surface under the cursor, else a plane facing the camera\n\t/// through the last point, else the ground.\n\t/// \u003C/summary\u003E\n\tprivate Vec3? SplineLanding( Vec3 origin, Vec3 direction, List\u003CVec3\u003E points, Vector3 cameraForward )\n\t{\n\t\tvar visible = _displayBodies.Where( b =\u003E b?.Mesh is not null \u0026\u0026 b.Visible ).ToList();\n\n\t\tif ( visible.Count \u003E 0 \u0026\u0026 MeshRaycast.Raycast( visible, origin, direction, PickTreeFor ) is { } hit )\n\t\t\treturn hit.Hit.Point;\n\n\t\tif ( points.Count \u003E 0 )\n\t\t{\n\t\t\tvar normal = new Vec3( cameraForward.x, cameraForward.y, cameraForward.z );\n\n\t\t\tif ( RayPlane( origin, direction, points[^1], normal, out var onView ) )\n\t\t\t\treturn onView;\n\t\t}\n\n\t\tif ( RayPlane( origin, direction, Vec3.Zero, new Vec3( 0, 0, 1 ), out var onGround ) )\n\t\t\treturn onGround;\n\n\t\treturn null;\n\t}\n\n\tprivate static bool RayPlane( Vec3 origin, Vec3 direction, Vec3 planePoint, Vec3 planeNormal, out Vec3 hit )\n\t{\n\t\thit = default;\n\n\t\tvar denominator = Vec3.Dot( direction, planeNormal );\n\n\t\tif ( MathF.Abs( denominator ) \u003C 1e-6f )\n\t\t\treturn false;\n\n\t\tvar t = Vec3.Dot( planePoint - origin, planeNormal ) / denominator;\n\n\t\tif ( t \u003C= 0f )\n\t\t\treturn false;\n\n\t\thit = origin \u002B direction * t;\n\t\treturn true;\n\t}\n}\n"},{"Ident":"pooh.geppetto","Path":"Editor/RigControlEditor/RigHelpBox.cs","FileName":"RigHelpBox.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":411088,"IsPrivate":false,"Code":"using Editor;\nusing Sandbox;\n\nnamespace Marionette.Tools;\n\n/// \u003Csummary\u003E\n/// The \u0022Documentation\u0022 style collapsible note used throughout the stock Inspector (a chevron, a\n/// circled ? badge, a bold title, a blue accent line under the whole block) - reused here so each\n/// tab opens with the same look instead of a hand-rolled one-off.\n/// \u003C/summary\u003E\ninternal static class RigHelpBox\n{\n\t/// \u003Csummary\u003EOne labelled chunk of the note - \u0022Model\u0022, \u0022IK Constraint\u0022, whatever the reader\n\t/// needs to look up on its own rather than hunt for inside one wall of text.\u003C/summary\u003E\n\tpublic readonly record struct Section( string Heading, string Body );\n\n\tpublic static Section S( string heading, string body ) =\u003E new( heading, body );\n\n\t/// \u003Csummary\u003E\n\t/// intro: a plain, unlabelled paragraph at the top - what this tab is, in one or two\n\t/// sentences. sections: each gets its own bold heading and paragraph below it, so a reader\n\t/// can scan headings instead of parsing a block of text to find the one field they care about.\n\t///\n\t/// COLLAPSED BY DEFAULT. Expanded, three tabs of reference text is the first thing a new user\n\t/// sees, before they\u0027ve done anything for it to be about - which reads as homework and gets\n\t/// skipped wholesale, taking the useful parts with it. The status bar\u0027s tutorial handles\n\t/// \u0022what do I do now\u0022 one step at a time; this is the reference you open once you have a\n\t/// specific question, and the header line says which question it answers.\n\t/// \u003C/summary\u003E\n\tpublic static Widget Create( Widget parent, string intro, Section[] sections, bool startExpanded = false )\n\t{\n\t\tvar expanded = startExpanded;\n\n\t\tvar box = new Widget( parent ) { Layout = Layout.Column() };\n\t\tbox.Layout.Margin = 0;\n\n\t\tbox.OnPaintOverride = () =\u003E\n\t\t{\n\t\t\t// Accent line along the bottom, the one clearly-visible border the reference has.\n\t\t\tPaint.ClearPen();\n\t\t\tPaint.SetBrush( Theme.Blue );\n\t\t\tPaint.DrawRect( new Rect( 0, box.Height - 2, box.Width, 2 ) );\n\t\t\treturn false;\n\t\t};\n\n\t\tvar header = new HeaderWidget( box ) { FixedHeight = 28 };\n\t\tbox.Layout.Add( header );\n\n\t\tvar body = new Widget( box ) { Layout = Layout.Column() };\n\t\tbody.Layout.Margin = new Sandbox.UI.Margin( 32, 0, 12, 12 );\n\t\tbody.Layout.Spacing = 10;\n\n\t\tif ( !string.IsNullOrWhiteSpace( intro ) )\n\t\t\tbody.Layout.Add( new Editor.Label( intro ) { WordWrap = true } );\n\n\t\tforeach ( var section in sections )\n\t\t{\n\t\t\tvar block = body.Layout.AddColumn();\n\t\t\tblock.Spacing = 2;\n\n\t\t\tvar heading = new Editor.Label( section.Heading ) { Color = Theme.Blue };\n\t\t\theading.SetStyles( \u0022font-weight: 600;\u0022 );\n\t\t\tblock.Add( heading );\n\n\t\t\tblock.Add( new Editor.Label( section.Body ) { WordWrap = true } );\n\t\t}\n\n\t\tbox.Layout.Add( body );\n\n\t\tvoid UpdateState()\n\t\t{\n\t\t\theader.Expanded = expanded;\n\t\t\tbody.Visible = expanded;\n\t\t}\n\n\t\tUpdateState();\n\n\t\theader.Clicked = () =\u003E\n\t\t{\n\t\t\texpanded = !expanded;\n\t\t\tUpdateState();\n\t\t};\n\n\t\treturn box;\n\t}\n\n\t/// \u003Csummary\u003EChevron \u002B circled \u0022?\u0022 \u002B bold \u0022Documentation\u0022 title, hand-painted so it actually\n\t/// matches the reference instead of approximating it with stock Label/Button widgets.\u003C/summary\u003E\n\tprivate sealed class HeaderWidget : Widget\n\t{\n\t\tpublic bool Expanded { get; set; } = true;\n\t\tpublic System.Action Clicked { get; set; }\n\n\t\tpublic HeaderWidget( Widget parent ) : base( parent )\n\t\t{\n\t\t\tCursor = CursorShape.Finger;\n\t\t}\n\n\t\tprotected override void OnPaint()\n\t\t{\n\t\t\tvar rect = LocalRect;\n\n\t\t\tPaint.Antialiasing = true;\n\n\t\t\t// Chevron\n\t\t\tPaint.ClearBrush();\n\t\t\tPaint.SetPen( Theme.Blue );\n\t\t\tPaint.DrawIcon( new Rect( 6, 0, 16, rect.Height ), Expanded ? \u0022expand_more\u0022 : \u0022chevron_right\u0022, 16, TextFlag.Center );\n\n\t\t\t// Circled \u0022?\u0022 badge - DrawCircle takes a Rect (confirmed from existing usage\n\t\t\t// elsewhere in this project), not a center\u002Bradius pair.\n\t\t\tvar badgeCenter = new Vector2( 34, rect.Center.y );\n\t\t\tPaint.ClearPen();\n\t\t\tPaint.SetBrush( Theme.Blue );\n\t\t\tPaint.DrawCircle( new Rect( badgeCenter.x - 8, badgeCenter.y - 8, 16, 16 ) );\n\n\t\t\tPaint.SetPen( Theme.WindowBackground );\n\t\t\tPaint.SetDefaultFont( 8, 700 );\n\t\t\tPaint.DrawText( new Rect( badgeCenter.x - 8, badgeCenter.y - 8, 16, 16 ), \u0022?\u0022, TextFlag.Center );\n\n\t\t\t// Title\n\t\t\tPaint.SetPen( Theme.Blue );\n\t\t\tPaint.SetDefaultFont( 8, 600 );\n\t\t\tPaint.DrawText( new Rect( 50, 0, rect.Width - 58, rect.Height ), \u0022Documentation\u0022, TextFlag.LeftCenter );\n\t\t}\n\n\t\tprotected override void OnMouseClick( MouseEvent e )\n\t\t{\n\t\t\tbase.OnMouseClick( e );\n\t\t\tClicked?.Invoke();\n\t\t}\n\t}\n}\n"},{"Ident":"pooh.geppetto","Path":"Effigy.Tests/BoneFromBodyTests.cs","FileName":"BoneFromBodyTests.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":411088,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing Effigy;\n\nnamespace Effigy.Tests;\n\n/// \u003Csummary\u003E\n/// Deriving a bone from the shape of the body it will move.\n///\n/// The theme here is that the answers must be STABLE, not merely plausible. A derived bone that is\n/// a little off is easy to nudge; a derived bone that comes out differently on two identical\n/// fingers, or flips end-for-end when the model is rebuilt, is worse than no feature at all,\n/// because the modeller has to check every one by eye before trusting any of them. So most of what\n/// follows measures repeatability and orientation rather than precision.\n/// \u003C/summary\u003E\npublic static class BoneFromBodyTests\n{\n\tpublic static void Run()\n\t{\n\t\tSection( \u0022a bone spans the body\u0027s longest axis\u0022 );\n\t\tTestLongestAxis();\n\n\t\tSection( \u0022roll follows the widest direction across the bone\u0022 );\n\t\tTestRoll();\n\n\t\tSection( \u0022an anchor decides which end is the head\u0022 );\n\t\tTestAnchor();\n\n\t\tSection( \u0022shapes with no answer are refused rather than guessed\u0022 );\n\t\tTestDegenerate();\n\n\t\tSection( \u0022the same shape always derives the same bone\u0022 );\n\t\tTestRepeatable();\n\n\t\tSection( \u0022a derived bone feeds Skeleton unchanged\u0022 );\n\t\tTestFeedsSkeleton();\n\t}\n\n\t/// \u003Csummary\u003EThe eight corners of an axis-aligned box from the origin to (sx, sy, sz).\u003C/summary\u003E\n\tstatic List\u003CVec3\u003E Box( float sx, float sy, float sz )\n\t{\n\t\tvar points = new List\u003CVec3\u003E();\n\n\t\tfor ( var i = 0; i \u003C 8; i\u002B\u002B )\n\t\t{\n\t\t\tpoints.Add( new Vec3(\n\t\t\t\t(i \u0026 1) == 0 ? 0 : sx,\n\t\t\t\t(i \u0026 2) == 0 ? 0 : sy,\n\t\t\t\t(i \u0026 4) == 0 ? 0 : sz ) );\n\t\t}\n\n\t\treturn points;\n\t}\n\n\tstatic void TestLongestAxis()\n\t{\n\t\t// Four long, two wide, one thick \u2014 a finger, near enough, and unambiguous about which axis\n\t\t// is which.\n\t\tvar ok = BoneFromBody.TryDerive( Box( 4, 1, 2 ), out var head, out var tail, out _ );\n\n\t\tCheck( \u0022a box derives a bone\u0022, ok );\n\t\tCheck( \u0022the bone aims down the long axis\u0022,\n\t\t\tNear( (tail - head).Normal, new Vec3( 1, 0, 0 ) ), (tail - head).Normal.ToString() );\n\t\tCheck( \u0022it spans the full extent\u0022, Near( (tail - head).Length, 4f ), (tail - head).Length.ToString() );\n\t\tCheck( \u0022it sits on the body\u0027s centre line\u0022, Near( head, new Vec3( 0, 0.5f, 1 ) ), head.ToString() );\n\n\t\t// Rotated onto a diagonal: nothing about the method is axis-aligned, and a body built on a\n\t\t// sketch plane that is not one of the three standard ones is the normal case, not the\n\t\t// exotic one.\n\t\tvar diagonal = new List\u003CVec3\u003E();\n\t\tvar along = new Vec3( 1, 1, 0 ).Normal;\n\n\t\tforeach ( var p in Box( 4, 1, 2 ) )\n\t\t{\n\t\t\t// Re-express the box about the diagonal: x runs along it, y across it in plane.\n\t\t\tdiagonal.Add( along * p.x \u002B new Vec3( -along.y, along.x, 0 ) * p.y \u002B new Vec3( 0, 0, 1 ) * p.z );\n\t\t}\n\n\t\tBoneFromBody.TryDerive( diagonal, out var dHead, out var dTail, out _ );\n\n\t\tCheck( \u0022a diagonal body aims down its own long axis\u0022,\n\t\t\tNear( (dTail - dHead).Normal, along ), (dTail - dHead).Normal.ToString() );\n\t\tCheck( \u0022and still spans four units\u0022, Near( (dTail - dHead).Length, 4f ),\n\t\t\t(dTail - dHead).Length.ToString() );\n\t}\n\n\tstatic void TestRoll()\n\t{\n\t\t// Long in x, thin in y, wide in z. The roll should name z, because that is the direction\n\t\t// the section actually has width in \u2014 it is what an animator would call the flat of it.\n\t\tBoneFromBody.TryDerive( Box( 4, 1, 2 ), out _, out _, out var up );\n\n\t\tCheck( \u0022roll takes the wide direction, not the thin one\u0022,\n\t\t\tNear( up, new Vec3( 0, 0, 1 ) ) || Near( up, new Vec3( 0, 0, -1 ) ), up.ToString() );\n\n\t\t// Swap which of the two cross directions is wider and the roll has to follow. This is the\n\t\t// check that would fail if the axes were simply being read off in a fixed order.\n\t\tBoneFromBody.TryDerive( Box( 4, 2, 1 ), out _, out _, out var swapped );\n\n\t\tCheck( \u0022widening the other way rolls the bone\u0022,\n\t\t\tNear( swapped, new Vec3( 0, 1, 0 ) ) || Near( swapped, new Vec3( 0, -1, 0 ) ),\n\t\t\tswapped.ToString() );\n\n\t\tBoneFromBody.TryDerive( Box( 4, 1, 2 ), out _, out var tail, out var perpendicular );\n\t\tBoneFromBody.TryDerive( Box( 4, 1, 2 ), out var head, out _, out _ );\n\n\t\tCheck( \u0022the hint is already perpendicular to the aim\u0022,\n\t\t\tMathF.Abs( Vec3.Dot( perpendicular, (tail - head).Normal ) ) \u003C 1e-4f,\n\t\t\tVec3.Dot( perpendicular, (tail - head).Normal ).ToString() );\n\t\tCheck( \u0022and is unit length\u0022, Near( perpendicular.Length, 1f ), perpendicular.Length.ToString() );\n\t}\n\n\tstatic void TestAnchor()\n\t{\n\t\tvar box = Box( 4, 1, 2 );\n\n\t\t// No anchor: canonical, and the canon is \u002Bx, so the head is the low end.\n\t\tBoneFromBody.TryDerive( box, out var head, out var tail, out _ );\n\t\tCheck( \u0022unanchored, the head is the canonical end\u0022, head.x \u003C tail.x, $\u0022{head} -\u003E {tail}\u0022 );\n\n\t\t// Anchored past the far end, the bone has to turn around \u2014 that end is where it joins the\n\t\t// rest of the model, and a bone growing back toward its own parent is the bug this stops.\n\t\tBoneFromBody.TryDerive( box, out var flipped, out var flippedTail, out _, new Vec3( 9, 0, 0 ) );\n\n\t\tCheck( \u0022an anchor at the far end swaps the ends\u0022, flipped.x \u003E flippedTail.x,\n\t\t\t$\u0022{flipped} -\u003E {flippedTail}\u0022 );\n\t\tCheck( \u0022swapping moves neither point\u0022, Near( flipped, tail ) \u0026\u0026 Near( flippedTail, head ),\n\t\t\t$\u0022{flipped} / {flippedTail}\u0022 );\n\n\t\t// An anchor on the side it already pointed at must change nothing at all.\n\t\tBoneFromBody.TryDerive( box, out var kept, out var keptTail, out _, new Vec3( -9, 0, 0 ) );\n\n\t\tCheck( \u0022an anchor at the near end leaves it alone\u0022,\n\t\t\tNear( kept, head ) \u0026\u0026 Near( keptTail, tail ), $\u0022{kept} -\u003E {keptTail}\u0022 );\n\t}\n\n\tstatic void TestDegenerate()\n\t{\n\t\tCheck( \u0022no points is refused\u0022,\n\t\t\t!BoneFromBody.TryDerive( new List\u003CVec3\u003E(), out _, out _, out _ ) );\n\t\tCheck( \u0022one point is refused\u0022,\n\t\t\t!BoneFromBody.TryDerive( new List\u003CVec3\u003E { Vec3.Zero }, out _, out _, out _ ) );\n\t\tCheck( \u0022a null list is refused\u0022,\n\t\t\t!BoneFromBody.TryDerive( (IReadOnlyList\u003CVec3\u003E)null, out _, out _, out _ ) );\n\t\tCheck( \u0022a null mesh is refused\u0022,\n\t\t\t!BoneFromBody.TryDerive( (PolyMesh)null, out _, out _, out _ ) );\n\n\t\t// Every point in the same place has no axis and no length. Refusing beats returning a\n\t\t// zero-length bone, which Skeleton would throw on anyway, further from the cause.\n\t\tvar stacked = new List\u003CVec3\u003E { new( 1, 1, 1 ), new( 1, 1, 1 ), new( 1, 1, 1 ) };\n\t\tCheck( \u0022a body with no extent is refused\u0022,\n\t\t\t!BoneFromBody.TryDerive( stacked, out _, out _, out _ ) );\n\n\t\t// A CUBE HAS NO LONGEST AXIS and that is not an error \u2014 every direction is a legitimate\n\t\t// principal axis, so there is nothing to be right about. It must still produce a usable\n\t\t// bone rather than a NaN or a refusal.\n\t\tvar cube = BoneFromBody.TryDerive( Box( 2, 2, 2 ), out var head, out var tail, out var up );\n\n\t\tCheck( \u0022a cube still derives a bone\u0022, cube );\n\t\tCheck( \u0022the cube\u0027s bone has real length\u0022,\n\t\t\tcube \u0026\u0026 (tail - head).Length \u003E 1e-3f \u0026\u0026 !float.IsNaN( (tail - head).Length ),\n\t\t\tcube ? (tail - head).Length.ToString() : \u0022refused\u0022 );\n\t\tCheck( \u0022and a real up-hint\u0022, cube \u0026\u0026 Near( up.Length, 1f ), up.Length.ToString() );\n\t}\n\n\tstatic void TestRepeatable()\n\t{\n\t\t// The property the whole feature leans on: a patterned row of eight fingers has to come out\n\t\t// as eight bones pointing the same way. Same shape in, same bone out, every time.\n\t\tBoneFromBody.TryDerive( Box( 4, 1, 2 ), out var headA, out var tailA, out var upA );\n\t\tBoneFromBody.TryDerive( Box( 4, 1, 2 ), out var headB, out var tailB, out var upB );\n\n\t\tCheck( \u0022two identical bodies derive identical bones\u0022,\n\t\t\tNear( headA, headB ) \u0026\u0026 Near( tailA, tailB ) \u0026\u0026 Near( upA, upB ) );\n\n\t\t// Order of the vertices is not part of the shape, and a rebuild is free to change it.\n\t\tvar shuffled = Box( 4, 1, 2 );\n\t\tshuffled.Reverse();\n\n\t\tBoneFromBody.TryDerive( shuffled, out var headC, out var tailC, out var upC );\n\n\t\tCheck( \u0022vertex order does not change the answer\u0022,\n\t\t\tNear( headA, headC ) \u0026\u0026 Near( tailA, tailC ) \u0026\u0026 Near( upA, upC ),\n\t\t\t$\u0022{headC} -\u003E {tailC}, up {upC}\u0022 );\n\n\t\t// Translated bodies are the same shape somewhere else, so the bone must simply move with\n\t\t// them \u2014 a pattern is a translation, and this is that case.\n\t\tvar moved = new List\u003CVec3\u003E();\n\t\tvar shift = new Vec3( 10, -3, 7 );\n\n\t\tforeach ( var p in Box( 4, 1, 2 ) )\n\t\t\tmoved.Add( p \u002B shift );\n\n\t\tBoneFromBody.TryDerive( moved, out var headD, out var tailD, out var upD );\n\n\t\tCheck( \u0022a translated body derives a translated bone\u0022,\n\t\t\tNear( headD, headA \u002B shift ) \u0026\u0026 Near( tailD, tailA \u002B shift ) \u0026\u0026 Near( upD, upA ),\n\t\t\t$\u0022{headD} -\u003E {tailD}\u0022 );\n\t}\n\n\tstatic void TestFeedsSkeleton()\n\t{\n\t\t// The output exists to be handed straight to AddBoneFromPoints, up-hint and all, so check\n\t\t// the bone that actually lands rather than only the numbers on the way in.\n\t\tBoneFromBody.TryDerive( Box( 4, 1, 2 ), out var head, out var tail, out var up );\n\n\t\tvar skeleton = new Skeleton();\n\t\tskeleton.AddBoneFromPoints( \u0022finger\u0022, -1, head, tail, up );\n\n\t\tvar world = skeleton.WorldBind( 0 );\n\n\t\tCheck( \u0022the bone\u0027s \u002BY runs down the body\u0022, Near( world.Y, new Vec3( 1, 0, 0 ) ), world.Y.ToString() );\n\t\tCheck( \u0022the bone\u0027s \u002BZ took the derived roll\u0022, Near( world.Z, up ), world.Z.ToString() );\n\t\tCheck( \u0022the bone starts at the derived head\u0022, Near( world.Origin, head ), world.Origin.ToString() );\n\t\tCheck( \u0022its length is the body\u0027s extent\u0022, Near( skeleton.Bones[0].Length, 4f ),\n\t\t\tskeleton.Bones[0].Length.ToString() );\n\t}\n\n\tstatic bool Near( Vec3 a, Vec3 b, float tolerance = 1e-4f ) =\u003E (a - b).Length \u003C tolerance;\n\n\tstatic bool Near( float a, float b, float tolerance = 1e-4f ) =\u003E MathF.Abs( a - b ) \u003C tolerance;\n\n\tstatic void Section( string title ) =\u003E Report.Section( title );\n\n\tstatic void Check( string what, bool ok, string detail = null ) =\u003E Report.Check( what, ok, detail );\n}\n"}]}