using UnityEngine; using UnityEngine.UI; using System; using System.Collections.Generic; using System.Globalization; using System.Text; using MeshVR; using MVR.FileManagementSecure; using System.Linq; using SimpleJSON; namespace Slimy { public class InstantMorph : MVRScript { class PairList : IEnumerable> { private readonly List> _items = new List>(); public void Add(int a, int b) { _items.Add(new KeyValuePair(a, b)); } // ← {a,b} を受ける public int Count { get { return _items.Count; } } public IEnumerator> GetEnumerator() { return _items.GetEnumerator(); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } } private JSONStorableString genParentUrl = new JSONStorableString( "Gen Parent URL", "/data/DAZ%203D/Genesis%202/Female/Anatomy/Genesis2FemaleGenitalia.dsf#GenesisFemaleGenitalia-1" ); //============================================================== // Save/Loadパラメータ //============================================================== private string _plugindir = "Saves/PluginData"; private string _pluginowner = "/Slimy"; private string _plugin = "/InstantMorph"; private string _dataFile = "savedata.json"; private string savePath = "Saves/PluginData/Slimy/InstantMorph/"; private string lastBrowseDir = "Saves/PluginData/Slimy/"; // ダミーパスでリスト更新 private string fileExt = "json"; private string ext = "json"; private static Material sharedEdgeMat; private Material sharedBlueMat; // 青マーカ共有 private Material sharedGreenMat; // 緑マーカ共有 ← これを追加 private Material sharedOrangeMat; // orange marker for proportional falloff private Dictionary proportionalAffectedVertices = new Dictionary(); DAZMergedSkinV2 skin; private JSONStorableBool genSelectOnly; Vector3[] baseVertices; private Vector3[] cachedVerts; private Vector3[] baseNormals; private Vector3[] meshNormals; private JSONStorableBool hoverDisplayJSON; private Vector3[] currentBaseVertices; Vector3[] dragDelta; private Vector3 _lastSliderOffset = Vector3.zero; private int pendingReset = -1; int dragIndex = -1; bool dragging = false; float dragStartDepth; Vector3[] originalVertices; // 初期位置保持用 private GameObject dragGhost = null; // ドラッグ中にマウス追従させるゴースト private LineRenderer dragLine; // --- 新增:用于笔刷拖拽的临时基准(避免修改 originalVertices) --- private Vector3[] sculptBaseVertices; private InputField _editableTextField; private List matchedMorphs = new List(); private List morphSliders = new List(); // === OPT: perf/throttle fields === float __lastSurfaceBuildTime = 0f; const float __SURFACE_BUILD_INTERVAL = 0.10f; // seconds bool __prevAlt = false, __prevCtrl = false; int _lastPaintVertex = -1; bool _lastAlt = false; bool _lastCtrl = false; JSONStorableStringChooser depthModeJSON; bool IsThrough() { return depthModeJSON != null && depthModeJSON.val == "Through"; } JSONStorableString textJSON; UIDynamicTextField textField; private float _globalMax = 1f; private InputField _maxInput; MeshCollider meshCollider; GameObject meshColliderContainer; GameObject markerRoot; GameObject hoverMarker; GameObject paintCircle; int[] meshTriangles; Vector3[] meshVertices; // rawVerts (markerRoot local space) HashSet selectedVertices = new HashSet(); Dictionary vertexMarkers = new Dictionary(); int lastHovered = -1; Dictionary baseMarkers = new Dictionary(); JSONStorableFloat brushRadiusSlider; JSONStorableFloat exportScaleSlider; JSONStorableBool originalVerticesJSON; // Export scale (applies to DSF deltas only) Vector3 lastHitPoint; Vector3 lastHitNormal; bool hasLastHit = false; //const int FIXED_VERTEX_COUNT = 21556; // DSF vertex_count(Female) const int FIXED_VERTEX_COUNT = 21344; // DSF vertex_count(Male) // === 平行移動値 === float _xVal = 0f; float _yVal = 0f; float _zVal = 0f; float _xOffset = 0f; float _yOffset = 0f; float _zOffset = 0f; JSONStorableFloat xOffsetSlider; JSONStorableFloat yOffsetSlider; JSONStorableFloat zOffsetSlider; // --- 新增:拖拽笔刷跟随(Follow brush)参数 --- JSONStorableFloat followRangeSlider; // Follow Range(局部/T-pose 单位) JSONStorableFloat followWeightSlider; // Follow Weight(0..1) private JSONStorableBool followWeightToggle; // 内部缓存值(实时读取 slider.val 更慢,缓存更快) private float _followRange = 0.05f; // 默认 5cm(你可根据需要调整) private float _followWeight = 1f; // 默认权重 1) // === スケール値 === float _sxVal = 1f, _syVal = 1f, _szVal = 1f; JSONStorableFloat xScaleSlider; JSONStorableFloat yScaleSlider; JSONStorableFloat zScaleSlider; // === 回転値 === float _xRot = 0f, _yRot = 0f, _zRot = 0f; JSONStorableFloat xRotateSlider; JSONStorableFloat yRotateSlider; JSONStorableFloat zRotateSlider; const float SYM_EPS = 1e-3f;//3fで普通2fだと緩い private static int Quantize(float v) { return Mathf.RoundToInt(v / SYM_EPS); } private JSONStorableBool autoSymmetryDSF; // 新增:当 autoSymmetryDSF 打开时,是否把对称 DSF 拆分为左右两个文件 private JSONStorableBool splitSymmetryDSF; private Dictionary symMapBody; private Dictionary symMapGen; private void BuildSymMaps() { int gender = GetGenderInt(containingAtom); if (gender == 1) { //SuperController.LogMessage("[InstantMorph] BuildSymMaps(): MALE detected"); symMapBody = SymPairsBody_male.Build(); symMapGen = SymPairsGen_male.Build(); //SuperController.LogMessage("[InstantMorph] Loaded male SymPairs (body/gen)"); //return; } if (gender == 0) { //SuperController.LogMessage("[InstantMorph] BuildSymMaps(): FEMALE detected"); symMapBody = SymPairsBody_female.Build(); symMapGen = SymPairsGen_female.Build(); //SuperController.LogMessage("[InstantMorph] Loaded female SymPairs (body/gen)"); //return; } //SuperController.LogMessage("[DEBUG] symMapBody.Count=" + (symMapBody == null ? -1 : symMapBody.Count)); //SuperController.LogError("[InstantMorph] BuildSymMaps(): gender detection failed."); } ////////////////////////////////////////////////////////////////////// public override void Init() { // --- gender-detection and SymPairs load --- int gender = GetGenderInt(containingAtom); // 0:female, 1:male, -1:unknown if (gender == 1) { SuperController.LogMessage("[InstantMorph] Detected gender: MALE"); try { SymPairsBody_male.Build(); SymPairsGen_male.Build(); } catch (System.Exception ex) { SuperController.LogError("[InstantMorph] Failed to load male SymPairs: " + ex.Message); } } else if (gender == 0) { SuperController.LogMessage("[InstantMorph] Detected gender: FEMALE"); try { SymPairsBody_female.Build(); SymPairsGen_female.Build(); } catch (System.Exception ex) { SuperController.LogError("[InstantMorph] Failed to load female SymPairs: " + ex.Message); } } else { SuperController.LogError("[InstantMorph] Gender detection failed; SymPairs not loaded."); } // --- end gender load --- string imgFilePath = GetPluginPath(this) + "/img/"; BuildSymMaps(); /* var purgeBtn = CreateButton("testButton", true); purgeBtn.button.onClick.AddListener(() => { LogSelectedVertexPositions_TPose(); //LogSelectedVertexPositions_Current(); }); */ //右カラム////////////////////////////////////// var row4 = CreateSpacer(true); row4.height =54f; var hlg4 = row4.gameObject.AddComponent(); hlg4.spacing = 0f; hlg4.childAlignment = TextAnchor.UpperCenter; hlg4.childForceExpandWidth = false; hlg4.childForceExpandHeight = false; // ボタン呼び出しだけ残す AddSaveButton(row4.transform, imgFilePath + "saveAll.png", () => { SaveAllSlots(); }); AddSaveButton(row4.transform, imgFilePath + "loadAll.png", () => { LoadAllSlots(); }); ////////////////////////////////////////////////////////////////// // ===== ボタン行 ===== var row = CreateSpacer(true); row.height =30f; var hlg = row.gameObject.AddComponent(); hlg.spacing = 10f; hlg.childAlignment = TextAnchor.UpperCenter; hlg.childForceExpandWidth = false; hlg.childForceExpandHeight = false; // ボタン呼び出しだけ残す AddSaveButton(row.transform, imgFilePath + "saveButton.png", () => { SaveSlot(1); }); AddSaveButton(row.transform, imgFilePath + "saveButton.png", () => { SaveSlot(2); }); AddSaveButton(row.transform, imgFilePath + "saveButton.png", () => { SaveSlot(3); }); AddSaveButton(row.transform, imgFilePath + "saveButton.png", () => { SaveSlot(4); }); AddSaveButton(row.transform, imgFilePath + "saveButton.png", () => { SaveSlot(5); }); AddSaveButton(row.transform, imgFilePath + "saveButton.png", () => { SaveSlot(6); }); ////////////////////////// var row2 = CreateSpacer(true); row2.height =80f; var hlg2 = row2.gameObject.AddComponent(); hlg2.spacing = 10f; hlg2.childAlignment = TextAnchor.UpperCenter; hlg2.childForceExpandWidth = false; hlg2.childForceExpandHeight = false; // ボタン呼び出しだけ残す AddSaveButton(row2.transform, imgFilePath + "restoreButton1.png", () => { RestoreSlot(1); }); AddSaveButton(row2.transform, imgFilePath + "restoreButton2.png" , () => { RestoreSlot(2); }); AddSaveButton(row2.transform, imgFilePath + "restoreButton3.png", () => { RestoreSlot(3); }); AddSaveButton(row2.transform, imgFilePath + "restoreButton4.png", () => { RestoreSlot(4); }); AddSaveButton(row2.transform, imgFilePath + "restoreButton5.png", () => { RestoreSlot(5); }); AddSaveButton(row2.transform, imgFilePath + "restoreButton6.png", () => { RestoreSlot(6); }); /////////////////////////// var row3 = CreateSpacer(true); row3.height =30f; var hlg3 = row3.gameObject.AddComponent(); hlg3.spacing = 10f; hlg3.childAlignment = TextAnchor.UpperCenter; hlg3.childForceExpandWidth = false; hlg3.childForceExpandHeight = false; // ボタン呼び出しだけ残す AddSaveButton(row3.transform, imgFilePath + "resetButton.png", () => { RestoreSelectionOnly(1); }); AddSaveButton(row3.transform, imgFilePath + "resetButton.png", () => { RestoreSelectionOnly(2); }); AddSaveButton(row3.transform, imgFilePath + "resetButton.png", () => { RestoreSelectionOnly(3); }); AddSaveButton(row3.transform, imgFilePath + "resetButton.png", () => { RestoreSelectionOnly(4); }); AddSaveButton(row3.transform, imgFilePath + "resetButton.png", () => { RestoreSelectionOnly(5); }); AddSaveButton(row3.transform, imgFilePath + "resetButton.png", () => { RestoreSelectionOnly(6); }); ////////////////////////// ////////////////////////////////////////////////// /* var purgeBtn = CreateButton("Purge Invalid Selections", true); purgeBtn.button.onClick.AddListener(() => { PurgeInvalidSelections(); }); */ //////////////////////////////////////////////////// var clearBtn = CreateButton("Clear Selection", true); clearBtn.button.onClick.AddListener(ClearAllSelection); /////////////////////////////////////////////////////// var row5 = CreateSpacer(true); row5.height =60f; var hlg5 = row5.gameObject.AddComponent(); hlg5.spacing = 10f; hlg5.childAlignment = TextAnchor.UpperCenter; hlg5.childForceExpandWidth = false; hlg5.childForceExpandHeight = false; // ボタン呼び出しだけ残す AddSaveButton(row5.transform, imgFilePath + "centerOnly.png", () => { KeepCenterVerticesBySymMap(); }); AddSaveButton(row5.transform, imgFilePath + "centerRemove.png", () => { RemoveCenterVerticesFromSelection(); }); AddSaveButton(row5.transform, imgFilePath + "leftOnly.png", () => { KeepLeftSideSelectionBySymMap(); }); AddSaveButton(row5.transform, imgFilePath + "rightOnly.png", () => { KeepRightSideSelectionBySymMap(); }); AddSaveButton(row5.transform, imgFilePath + "mirrorX.png", () => { MirrorSelectedVertices(); }); ////////////////////////////////////////////////////////// /* var symBtn = CreateButton("Mirror X Flip", true); symBtn.button.onClick.AddListener(() => { MirrorSelectedVertices(); //SuperController.LogMessage("Mirror X Flip"); }); */ /////////////////////////////////////////////////////// //UIDynamicButton btnKeepLeft = CreateButton("One Side Only", true); //btnKeepLeft.button.onClick.AddListener(KeepLeftSideSelectionBySymMap); /* UIDynamicButton bLeft = CreateButton("Left Side Only"); bLeft.button.onClick.AddListener(() => KeepLeftSideSelectionBySymMap()); UIDynamicButton bRight = CreateButton("Right Side Only"); bRight.button.onClick.AddListener(() => KeepRightSideSelectionBySymMap()); */ ////////////////////////////////////////////////////////////////// //UIDynamicButton btnKeepCenter = CreateButton("Center Only", true); //btnKeepCenter.button.onClick.AddListener(KeepCenterVerticesBySymMap); //////////////////////////////////////////////////// //片側だけの頂点編集をもとに、左右対称モーフを出力する autoSymmetryDSF = new JSONStorableBool("Auto symmetry DSF", true); RegisterBool(autoSymmetryDSF); CreateToggle(autoSymmetryDSF, true); // 新增:拆分对称 DSF 的开关(仅在 Auto symmetry DSF 打开时才有意义) splitSymmetryDSF = new JSONStorableBool("Split symmetry DSF (Left/Right files)", false); RegisterBool(splitSymmetryDSF); // 将该 Toggle 放在 UI 中(默认 false) CreateToggle(splitSymmetryDSF, true); ////////////////////////////////////////////////// CreateButton("Generate Morph(.dsf)",true).button.onClick.AddListener(ExportMorphDSF); /////////////////////////////////////////////// //Morphフィルターワード入力フィールド var tf = CreateTextField(new JSONStorableString(name, "Filtering Word"), true); _editableTextField = tf.gameObject.AddComponent(); _editableTextField.textComponent = tf.UItext; _editableTextField.text = "Instant"; tf.UItext.fontSize = 36; tf.backgroundColor = Color.white; var layout = tf.GetComponent(); if (layout == null) layout = tf.gameObject.AddComponent(); layout.minHeight = 0f; layout.preferredHeight = 48f; // ←高さを1行化 _editableTextField.onValueChanged.AddListener(value => { SuperController.LogMessage("Search Morph by " + _editableTextField.text); FilterMorphs(value); }); ///////////////////////////////////////////////// // ▼ Morph再リスト///// var reloadBtn = CreateButton("Reload Instant Morphs", true); reloadBtn.button.onClick.AddListener(() => { FilterMorphs(_editableTextField.text); SuperController.LogMessage("Refresh Instant Morphs by" + _editableTextField.text); BuildMorphSliders(); }); ///////////////////////////////////////////////// //スライダーMax値の設定 // Init() 内:共通Max入力欄 /* var maxTF = CreateTextField(new JSONStorableString(name, "Morph Max"), true); _maxInput = maxTF.gameObject.AddComponent(); _maxInput.textComponent = maxTF.UItext; _maxInput.text = _globalMax.ToString(System.Globalization.CultureInfo.InvariantCulture); maxTF.UItext.fontSize = 28; maxTF.backgroundColor = Color.white; var maxLayout = maxTF.GetComponent(); if (maxLayout == null) maxLayout = maxTF.gameObject.AddComponent(); maxLayout.preferredHeight = 40f; _maxInput.onEndEdit.AddListener(val => { float f; if (float.TryParse(val, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out f)) { _globalMax = f; BuildMorphSliders(); // ← 再生成 } else { SuperController.LogError("Invalid Morph Max: " + val); _maxInput.text = _globalMax.ToString(System.Globalization.CultureInfo.InvariantCulture); } }); */ /////////////////////////////////////////////////////////// //左カラム//////////////////////////////////// CreateButton("Re-Bake CurrentPose as BaseMesh", false).button.onClick.AddListener(() => { CaptureCurrentPose(); }); //////////////////////////////////////////////////////// //Gen頂点だけを拾うチェックボックス(既定OFF=Body優先) genSelectOnly = new JSONStorableBool("Genital vertices only", false); //CreateToggle(genSelectOnly, false); var genSelectMode = new JSONStorableStringChooser( "Target Mesh", new List() { "Body", "Genital" }, genSelectOnly.val ? "Genital" : "Body", "Target Mesh" ); CreatePopup(genSelectMode, false); // ★ プルダウン選択で Bool を切り替え genSelectMode.setCallbackFunction = (string val) => { genSelectOnly.val = (val == "Genital"); }; //////////////////////////////////////////////////////// hoverDisplayJSON = new JSONStorableBool("Show Hover", true); //CreateToggle(hoverDisplayJSON, false); // 青マーカ用トグル originalVerticesJSON = new JSONStorableBool("Show Original vertices (blue)", true); RegisterBool(originalVerticesJSON); //CreateToggle(originalVerticesJSON); // コメントアウト //originalVerticesJSON.setCallbackFunction(true); originalVerticesJSON.setCallbackFunction = (bool val) => { foreach (var kv in baseMarkers) UnityEngine.Object.Destroy(kv.Value); baseMarkers.Clear(); if (val) { foreach (int index in selectedVertices) { GameObject marker = GameObject.CreatePrimitive(PrimitiveType.Quad); UnityEngine.Object.DestroyImmediate(marker.GetComponent()); marker.layer = LayerMask.NameToLayer("Ignore Raycast"); marker.transform.localScale = Vector3.one * 0.0010f;//青マーカのサイズ marker.transform.SetParent(markerRoot.transform, false); marker.transform.localPosition = originalVertices[index]; // ★必ず original を参照 var mat = new Material(Shader.Find("Battlehub/RTGizmos/Handles")); if (mat != null) { mat.SetFloat("_Offset", 1f); mat.SetFloat("_MinAlpha", 1f); mat.color = Color.blue; var rend = marker.GetComponent(); rend.material = mat; rend.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; rend.receiveShadows = false; // ★ 青マーカは奥に描画されるよう固定 rend.material.renderQueue = 2999; } baseMarkers[index] = marker; } } }; /////////////////////// // screen-relative brush radius brushRadiusSlider = new JSONStorableFloat("Brush Radius (screen)", 0.03f, 0.001f, 0.2f, true, true); RegisterFloat(brushRadiusSlider); CreateSlider(brushRadiusSlider); ///////////////////// var modes = new List { "FrontOnly", "Through" }; depthModeJSON = new JSONStorableStringChooser("PaintDepth", modes, "FrontOnly", "Brush Type"); RegisterStringChooser(depthModeJSON); CreatePopup(depthModeJSON, false); //////////////////// //////////////////////////////////////////////////////////////////////////////////////////// //followWeightToggle = new JSONStorableBool("Propotional Edit Mode", _followWeight > 0.5f); followWeightToggle = new JSONStorableBool("Propotional Edit Mode", false); RegisterBool(followWeightToggle); var toggleUI = CreateToggle(followWeightToggle); toggleUI.label = "Propotional Edit Mode"; // 双方向連動 followWeightToggle.setCallbackFunction = (bool val) => { _followWeight = val ? 1f : 0f; followWeightSlider.val = _followWeight; }; // Follow brush sliders(拖拽笔刷跟随) followRangeSlider = new JSONStorableFloat("Propotional Falloff (m)", _followRange, 0.001f, 1.0f, true, true); RegisterFloat(followRangeSlider); var followRangeUI = CreateSlider(followRangeSlider); followRangeUI.label = "Propotional Falloff (m)"; followRangeUI.valueFormat = "F4"; // 本地坐标/TPose 单位,单位为米 //followWeightSlider = new JSONStorableFloat("Propotional Edit", _followWeight, 0f, 1f, true, true); followWeightSlider = new JSONStorableFloat("Propotional Edit", 0f, 0f, 1f, true, true); //RegisterFloat(followWeightSlider); //var followWeightUI = CreateSlider(followWeightSlider); //followWeightUI.label = "Propotional Edit"; // 同步到内部缓存(如果需要立即响应 UI 改变) followRangeSlider.setCallbackFunction = (float _) => { _followRange = Mathf.Max(0.000001f, followRangeSlider.val); }; followWeightSlider.setCallbackFunction = (float _) => { _followWeight = Mathf.Clamp01(followWeightSlider.val); }; // 初始化缓存(确保默认值生效) _followRange = followRangeSlider != null ? followRangeSlider.val : _followRange; _followWeight = followWeightSlider != null ? followWeightSlider.val : _followWeight; ////////////////////////////////////////////////////////////////////////////////// // X offset (meters) - visual preview xOffsetSlider = new JSONStorableFloat("X Offset", 0f, -0.2f, 0.2f, true); RegisterFloat(xOffsetSlider); var xOffsetUI = CreateSlider(xOffsetSlider, false); xOffsetUI.label = "X Offset"; xOffsetUI.valueFormat = "F4"; // Y offset slider yOffsetSlider = new JSONStorableFloat("Y Offset", 0f, -0.2f, 0.2f, true); RegisterFloat(yOffsetSlider); var yOffsetUI = CreateSlider(yOffsetSlider, false); yOffsetUI.label = "Y Offset"; yOffsetUI.valueFormat = "F4"; // Z offset slider zOffsetSlider = new JSONStorableFloat("Z Offset", 0f, -0.2f, 0.2f, true); RegisterFloat(zOffsetSlider); var zOffsetUI = CreateSlider(zOffsetSlider, false); zOffsetUI.label = "Z Offset"; zOffsetUI.valueFormat = "F4"; _lastSliderOffset = new Vector3( xOffsetSlider != null ? xOffsetSlider.val : 0f, yOffsetSlider != null ? yOffsetSlider.val : 0f, zOffsetSlider != null ? zOffsetSlider.val : 0f ); // X scale: pivot is x=0 xScaleSlider = new JSONStorableFloat("X Scale (unsaved)", 1f, 0f, 2f, true); RegisterFloat(xScaleSlider); var xScaleUI = CreateSlider(xScaleSlider, false); xScaleUI.valueFormat = "F4"; // Y scale yScaleSlider = new JSONStorableFloat("Y Scale (unsaved)", 1f, 0f, 2f, true); RegisterFloat(yScaleSlider); var yScaleUI = CreateSlider(yScaleSlider, false); yScaleUI.valueFormat = "F4"; // Z scale zScaleSlider = new JSONStorableFloat("Z Scale (unsaved)", 1f, 0f, 2f, true); RegisterFloat(zScaleSlider); var zScaleUI = CreateSlider(zScaleSlider, false); zScaleUI.valueFormat = "F4"; ////////////////////// // === 回転スライダー === xRotateSlider = new JSONStorableFloat("X Rotate", 0f, -180f, 180f, true); RegisterFloat(xRotateSlider); var xRotateUI = CreateSlider(xRotateSlider, false); xRotateUI.valueFormat = "F4"; yRotateSlider = new JSONStorableFloat("Y Rotate", 0f, -180f, 180f, true); RegisterFloat(yRotateSlider); var yRotateUI = CreateSlider(yRotateSlider, false); yRotateUI.valueFormat = "F4"; zRotateSlider = new JSONStorableFloat("Z Rotate", 0f, -180f, 180f, true); RegisterFloat(zRotateSlider); var zRotateUI = CreateSlider(zRotateSlider, false); zRotateUI.valueFormat = "F4"; // 既存の setCallbackFunction 行を置き換え xOffsetSlider.setCallbackFunction = (float _) => { _xOffset = xOffsetSlider.val; ApplyXYOffsetsToSelected(); }; yOffsetSlider.setCallbackFunction = (float _) => { _yOffset = yOffsetSlider.val; ApplyXYOffsetsToSelected(); }; zOffsetSlider.setCallbackFunction = (float _) => { _zOffset = zOffsetSlider.val; ApplyXYOffsetsToSelected(); }; xScaleSlider.setCallbackFunction = (float _) => { _sxVal = xScaleSlider.val; ApplyXYOffsetsToSelected(); }; yScaleSlider.setCallbackFunction = (float _) => { _syVal = yScaleSlider.val; ApplyXYOffsetsToSelected(); }; zScaleSlider.setCallbackFunction = (float _) => { _szVal = zScaleSlider.val; ApplyXYOffsetsToSelected(); }; xRotateSlider.setCallbackFunction = (float _) => { _xRot = xRotateSlider.val; ApplyXYOffsetsToSelected(); }; yRotateSlider.setCallbackFunction = (float _) => { _yRot = yRotateSlider.val; ApplyXYOffsetsToSelected(); }; zRotateSlider.setCallbackFunction = (float _) => { _zRot = zRotateSlider.val; ApplyXYOffsetsToSelected(); }; // Export scale exportScaleSlider = new JSONStorableFloat("Export Delta Scale", 100f, 1f, 200f, true, true); //RegisterFloat(exportScaleSlider); //CreateSlider(exportScaleSlider).label = "Export Delta Scale (x)"; /////////////////////////////////////// brushRadiusSlider.setCallbackFunction = (float val) => { if (paintCircle != null && hasLastHit) { float worldRadius = ScreenSpaceToWorldRadius(lastHitPoint, val * 1000f); UpdatePaintCircle(lastHitPoint, lastHitNormal, worldRadius); } }; try { skin = containingAtom.GetComponentInChildren(); if (skin == null) { SuperController.LogError("No DAZMergedSkinV2 found."); return; } var rawVerts = skin.rawVertsBuffer; meshVertices = new Vector3[rawVerts.count]; rawVerts.GetData(meshVertices, 0, 0, rawVerts.count); // ★ 不変コピーを初期化 originalVertices = (Vector3[])meshVertices.Clone(); Mesh mesh = new Mesh(); mesh.vertices = meshVertices; mesh.triangles = skin.dazMesh.baseTriangles; meshTriangles = mesh.triangles; meshColliderContainer = CreateChildObject(containingAtom.gameObject, "VertexMultiLogger_MeshCollider"); meshColliderContainer.layer = LayerMask.NameToLayer("Ignore Raycast"); meshCollider = meshColliderContainer.AddComponent(); meshColliderContainer.layer = LayerMask.NameToLayer("Ignore Raycast"); meshCollider.sharedMesh = mesh; meshCollider.isTrigger = true; meshCollider.convex = false; meshCollider.enabled = true; markerRoot = CreateChildObject(containingAtom.gameObject, "VertexMarkers"); markerRoot.layer = LayerMask.NameToLayer("Ignore Raycast"); hoverMarker = CreateMarker(Color.yellow, 0.0012f); hoverMarker.name = "HoverMarker"; hoverMarker.transform.SetParent(markerRoot.transform, false); hoverMarker.SetActive(false); paintCircle = CreateRingMarker(Color.red, 0.01f, 0.0006f, 64); paintCircle.name = "PaintCircle"; paintCircle.transform.SetParent(markerRoot.transform, false); paintCircle.SetActive(false); //SuperController.LogMessage("VertexMultiSelectLogger ready."); // ポーズ前の頂点をクローン baseVertices = (Vector3[])skin.dazMesh.baseVertices.Clone(); baseNormals = (Vector3[])skin.dazMesh.baseNormals.Clone(); meshNormals = new Vector3[skin.normalsBuffer.count]; } catch (Exception e) { SuperController.LogError("Init error: " + e); } } //INITここまで/////////////////////////////////////////////////// ///////////////////////////////////////////////////// private void Update() { Camera cam = GetActiveCamera(); if (cam == null) { ResetHover(); return; } bool ctrl = Input.GetKey(KeyCode.LeftControl); bool alt = Input.GetKey(KeyCode.LeftAlt); bool __altReleased = (__prevAlt && !alt); bool __ctrlReleased = (__prevCtrl && !ctrl); __prevAlt = alt; __prevCtrl = ctrl; if (__altReleased || __ctrlReleased) { paintCircle.SetActive(false); // ★ 緑マーカの状態から selectedVertices を再構築(これだけ追加) if (vertexMarkers != null && selectedVertices != null) { selectedVertices.Clear(); foreach (var kv in vertexMarkers) if (kv.Value != null && kv.Value.activeInHierarchy) selectedVertices.Add(kv.Key); } BuildSelectionSurface(); } // 無入力スキップ if ( !Input.GetMouseButton(0) && !Input.GetMouseButton(1) && !Input.GetMouseButtonUp(0) && !Input.GetMouseButtonUp(1) && !Input.GetMouseButtonDown(0) && !Input.GetMouseButtonDown(1) && !Input.anyKey ) return; // ==== 青マーカ右クリックでリセット ==== if (!dragging && Input.GetMouseButtonUp(1) && pendingReset >= 0) { meshVertices[pendingReset] = originalVertices[pendingReset]; GameObject green = null; if (vertexMarkers.TryGetValue(pendingReset, out green) && green != null) green.transform.localPosition = originalVertices[pendingReset]; pendingReset = -1; BuildSelectionSurface(); } // ===== ドラッグ中 ===== if (dragging && dragIndex >= 0) { HandleDragging(cam); } // ===== MouseUp検出(ドラッグ終了)===== if (dragging && Input.GetMouseButtonUp(0)) { BuildSelectionSurface(); if (dragLine != null) dragLine.enabled = false; dragging = false; // Reset all vertex markers back to green after dragging ends ResetProportionalFalloffColors(); // ★ dragIndexを退避してからリセットする int id = dragIndex; // 修正後(C#6互換) GameObject marker; if (id >= 0 && vertexMarkers.TryGetValue(id, out marker) && marker != null) meshVertices[id] = marker.transform.localPosition; dragIndex = -1; SetAllFreeControllersCanGrab(true); // === 追加:ドラッグ終了時に頂点IDとペア情報をログ出力 === if (id >= 0) { BuildSymMaps(); bool genOnly = (genSelectOnly != null && genSelectOnly.val); var map = genOnly ? symMapGen : symMapBody; string pairText = "-"; if (map != null && map.ContainsKey(id)) { int pair = map[id]; if (pair == id) pairText = "center"; else pairText = pair.ToString(); } //SuperController.LogMessage($"[Drag] ID:{id} ({pairText})"); } } // ===== ペイント処理 ===== HandlePaint(cam, ctrl, alt); goto HOVER_ONLY; HOVER_ONLY: if (hoverDisplayJSON != null && !hoverDisplayJSON.val) return; int candidate = -1; float bestPx2 = float.MaxValue; Vector3 bestLocal = Vector3.zero; float thresholdPx = 10f; float thr2 = thresholdPx * thresholdPx; // 緑マーカ探索 if (selectedVertices != null && selectedVertices.Count > 0) { Vector3 mouse = Input.mousePosition; foreach (int vid in selectedVertices) { Vector3 local = meshVertices[vid]; GameObject mkTmp = null; if (vertexMarkers.TryGetValue(vid, out mkTmp) && mkTmp != null) local = mkTmp.transform.localPosition; Vector3 world = markerRoot.transform.TransformPoint(local); Vector3 sp = cam.WorldToScreenPoint(world); Vector2 dv = new Vector2(sp.x - mouse.x, sp.y - mouse.y); float d2 = dv.x * dv.x + dv.y * dv.y; if (d2 < bestPx2 && d2 <= thr2 && sp.z > 0f) { bestPx2 = d2; candidate = vid; bestLocal = local; } } } // 青マーカ探索 if (originalVerticesJSON != null && originalVerticesJSON.val && baseMarkers.Count > 0) { Vector3 mouse = Input.mousePosition; foreach (var kv in baseMarkers) { int vid = kv.Key; GameObject mkB = kv.Value; if (mkB == null) continue; Vector3 world = markerRoot.transform.TransformPoint(mkB.transform.localPosition); Vector3 sp = cam.WorldToScreenPoint(world); Vector2 dv = new Vector2(sp.x - mouse.x, sp.y - mouse.y); float d2 = dv.x * dv.x + dv.y * dv.y; if (d2 < bestPx2 && d2 <= thr2 && sp.z > 0f) { bestPx2 = d2; candidate = vid; bestLocal = mkB.transform.localPosition; } } } // ===== 左クリック(ドラッグ開始) or 右クリック(リセット) ===== if (candidate >= 0 && !ctrl && !alt) { if (Input.GetMouseButtonDown(0)) { // 左クリック = 緑マーカドラッグ開始 GameObject mkGreen; if (vertexMarkers.TryGetValue(candidate, out mkGreen) && mkGreen != null) { dragIndex = candidate; // ★★★ 追加:ドラッグ基準を現在形状でスナップ(毎回originalに戻るのを防ぐ) ★★★ if (meshVertices != null) { if (sculptBaseVertices == null || sculptBaseVertices.Length != meshVertices.Length) sculptBaseVertices = new Vector3[meshVertices.Length]; System.Array.Copy(meshVertices, sculptBaseVertices, meshVertices.Length); } dragging = true; SetAllFreeControllersCanGrab(false); Vector3 startWorld = markerRoot.transform.TransformPoint(mkGreen.transform.localPosition); dragStartDepth = cam.WorldToScreenPoint(startWorld).z; return; } } else if (Input.GetMouseButtonDown(1)) { // 右クリック = 青マーカリセット予約 GameObject mkBlue; if (baseMarkers.TryGetValue(candidate, out mkBlue) && mkBlue != null) { pendingReset = candidate; return; } } } // ===== 通常スキンhover ===== if (meshCollider == null) { ResetHover(); return; } { Ray ray = cam.ScreenPointToRay(Input.mousePosition); int lm = 1 << meshCollider.gameObject.layer; RaycastHit hit; bool got = meshCollider.Raycast(ray, out hit, 100f); if (!got) { float sphere = Mathf.Max(0.0005f, (brushRadiusSlider != null ? brushRadiusSlider.val * 0.25f : 0.005f)); got = Physics.SphereCast(ray, sphere, out hit, 100f, lm, QueryTriggerInteraction.Ignore); if (!got) { ResetHover(); return; } } int triIndex = hit.triangleIndex; if (triIndex >= 0) { int v0 = meshTriangles[triIndex * 3]; lastHovered = v0; } } } //Updateここまで////////////////////////////////////////////////////// private Camera GetActiveCamera() { Camera cam = Camera.main; if (cam != null) return cam; var cams = Camera.allCameras; for (int i = 0; i < cams.Length; i++) { if (cams[i] != null && cams[i].enabled) return cams[i]; } return GameObject.FindObjectOfType(); } private float ScreenSpaceToWorldRadius(Vector3 centerWorld, float screenRadiusPixels) { Camera cam = GetActiveCamera(); if (cam == null) return 0.01f; // フォールバック Vector3 sc = cam.WorldToScreenPoint(centerWorld); // center のスクリーン座標(zは深度) Vector3 se = new Vector3(sc.x + screenRadiusPixels, sc.y, sc.z); // 同じ z を維持して横にずらす Vector3 we = cam.ScreenToWorldPoint(se); // その位置をワールドへ return Vector3.Distance(centerWorld, we); } ////////////////////////////////////////////////// private void UpdatePaintCircle(Vector3 center, Vector3 normal, float radius) { if (paintCircle == null) return; var lr = paintCircle.GetComponent(); if (lr == null) return; int segments = lr.positionCount; if (segments < 3) segments = 3; Vector3[] points = new Vector3[segments]; Vector3 n = normal.normalized; //Vector3 n = Camera.main != null ? Camera.main.transform.forward : Vector3.forward; Vector3 tangent = Vector3.Cross(n, Vector3.up); if (tangent.sqrMagnitude < 1e-6f) tangent = Vector3.Cross(n, Vector3.right); tangent.Normalize(); Vector3 bitangent = Vector3.Cross(n, tangent); for (int i = 0; i < segments; i++) { float angle = 2 * Mathf.PI * i / segments; Vector3 dir = Mathf.Cos(angle) * tangent + Mathf.Sin(angle) * bitangent; points[i] = center + dir * radius; } lr.SetPositions(points); } private GameObject CreateSelectionMarker(int index) { GameObject marker = CreateMarker(Color.green, 0.0001f); marker.transform.SetParent(markerRoot.transform, false); // --- 位置補正 --- float xOff = xOffsetSlider != null ? xOffsetSlider.val : 0f; marker.transform.localPosition = meshVertices[index] + new Vector3(xOff, 0f, 0f); // --- 緑マーカは青より手前に描画する --- var rend = marker.GetComponent(); if (rend != null) { if (rend.sharedMaterial == null) rend.sharedMaterial = new Material(Shader.Find("Battlehub/RTGizmos/Handles")); rend.sharedMaterial.color = Color.green; rend.sharedMaterial.renderQueue = 3200; // 青が3100ならこれで手前 } vertexMarkers[index] = marker; return marker; } private GameObject CreateMarker(Color color, float scale) { GameObject marker = GameObject.CreatePrimitive(PrimitiveType.Quad); var __col = marker.GetComponent(); if (__col) UnityEngine.Object.DestroyImmediate(__col); marker.transform.localScale = Vector3.one * scale; var mat = new Material(Shader.Find("Battlehub/RTGizmos/Handles")); if (mat != null) { mat.SetFloat("_Offset", 1f); mat.SetFloat("_MinAlpha", 1f); mat.color = color; var rend = marker.GetComponent(); rend.material = mat; rend.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; rend.receiveShadows = false; } return marker; marker.layer = LayerMask.NameToLayer("Ignore Raycast"); } private GameObject CreateRingMarker(Color color, float radius, float lineWidth = 0.0005f, int segments = 64) { GameObject go = new GameObject("RingMarker"); var lr = go.AddComponent(); go.layer = LayerMask.NameToLayer("Ignore Raycast"); lr.useWorldSpace = true; lr.loop = true; lr.positionCount = Mathf.Max(segments, 3); lr.startWidth = lineWidth; lr.endWidth = lineWidth; lr.numCornerVertices = 4; lr.numCapVertices = 2; lr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; lr.receiveShadows = false; lr.alignment = LineAlignment.View; var mat = new Material(Shader.Find("Sprites/Default")); mat.color = color; lr.material = mat; UpdatePaintCircle(Vector3.zero, Vector3.forward, radius); return go; } private void RemoveMarkerAt(int index) { if (vertexMarkers.ContainsKey(index)) { UnityEngine.Object.Destroy(vertexMarkers[index]); vertexMarkers.Remove(index); } } private void ResetHover() { hoverMarker.SetActive(false); //paintCircle.SetActive(false);///////////////////////////////////////これのせいで外に出れない lastHovered = -1; } ////////////////////////////////////////////// private void OnDisable() { try { // ==== 保険:残存DragLineを削除 ==== foreach (var go in UnityEngine.Object.FindObjectsOfType()) { if (go != null && (go.name == "DragLine" || go.name.Contains("Marker"))) UnityEngine.Object.Destroy(go); } StopAllCoroutines(); // ==== 緑マーカ削除 ==== if (vertexMarkers != null) { foreach (var kv in vertexMarkers) { if (kv.Value != null) UnityEngine.Object.Destroy(kv.Value); } vertexMarkers.Clear(); } // ==== 青マーカ削除 ==== if (baseMarkers != null) { foreach (var kv in baseMarkers) { if (kv.Value != null) UnityEngine.Object.Destroy(kv.Value); } baseMarkers.Clear(); } // ==== 黄色ライン ==== if (dragLine != null) { UnityEngine.Object.Destroy(dragLine.gameObject); dragLine = null; } if (meshColliderContainer != null) UnityEngine.Object.Destroy(meshColliderContainer); if (markerRoot != null) UnityEngine.Object.Destroy(markerRoot); selectedVertices?.Clear(); meshVertices = null; originalVertices = null; meshNormals = null; } catch { } } //////////////////////////////////////////// private GameObject CreateChildObject(GameObject parent, string name) { GameObject go = new GameObject(name); go.transform.SetParent(parent.transform, false); go.layer = LayerMask.NameToLayer("Ignore Raycast"); return go; } // ===== DSF Export ======================================================== //========================================================================= private void ExportMorphDSF() { if (selectedVertices == null || selectedVertices.Count == 0) { SuperController.LogError("ExportMorphDSF: No selected vertices."); return; } const float EPS = 1e-14f; float scale = exportScaleSlider != null ? exportScaleSlider.val : 1f; // Body/Gen境界 const int BODY_MAX = 21556; // Body: 0–21555 const int GEN_COUNT = 1414; // Gen: 21556–22969 // 現在のプルダウン(Body/Gen選択) bool genOnly = (genSelectOnly != null && genSelectOnly.val); // 性別判定(0:female / 1:male) int gender = GetGenderInt(containingAtom); string gtxt = (gender == 1) ? "MALE" : (gender == 0 ? "FEMALE" : "UNKNOWN"); SuperController.LogMessage($"[Export] gender={gtxt}, target={(genOnly ? "Gen" : "Body")}"); // 出力対象のみ処理 ExportMorphDSF_Internal(genOnly, scale, EPS, BODY_MAX, GEN_COUNT); } //============================================================== // 内部処理本体(genOnly = true なら genitalia用) //============================================================== private void ExportMorphDSF_Internal(bool genOnly, float scale, float EPS, int bodyMax, int genCount) { try { if (skin != null && skin.normalsBuffer != null && meshNormals != null) skin.normalsBuffer.GetData(meshNormals, 0, 0, meshNormals.Length); BuildSymMaps(); Dictionary symMap = genOnly ? symMapGen : symMapBody; if (symMap == null) { SuperController.LogError("ExportMorphDSF_Internal: symMap is null"); return; } if (originalVertices == null || meshVertices == null) { SuperController.LogError("ExportMorphDSF_Internal: vertex arrays not initialized"); return; } int gender = GetGenderInt(containingAtom); // 0=female, 1=male bool isMale = (gender == 1); int vertexCount = genOnly ? 1414 : 21556; System.IFormatProvider ic = System.Globalization.CultureInfo.InvariantCulture; int maxLen = Mathf.Min(originalVertices.Length, meshVertices.Length); Quaternion rotTP = ComputeTPoseRotation(new List(selectedVertices)); Quaternion invRotTP = Quaternion.Inverse(rotTP); bool doSplit = (autoSymmetryDSF != null && autoSymmetryDSF.val) && (splitSymmetryDSF != null && splitSymmetryDSF.val); // ---- 保留原有单文件逻辑(当不做拆分时) ---- if (!doSplit) { List deltaLines = new List(); HashSet usedIds = new HashSet(); foreach (int idx in selectedVertices) { if (idx < 0 || idx >= maxLen) continue; if (genOnly && idx < bodyMax) continue; if (!genOnly && idx >= bodyMax) continue; Vector3 cur; GameObject go; if (vertexMarkers != null && vertexMarkers.TryGetValue(idx, out go) && go != null) cur = go.transform.localPosition; else cur = meshVertices[idx]; Vector3 orig = originalVertices[idx]; if (!IsFinite3(cur) || !IsFinite3(orig)) continue; Vector3 dispCur = cur - orig; if (!IsFinite3(dispCur) || dispCur.sqrMagnitude < EPS) continue; Vector3 dTP = invRotTP * dispCur; Vector3 d = new Vector3(-dTP.x, dTP.y, dTP.z) * 100f; // 中央顶点 x 归零(若开启自动对称) if (autoSymmetryDSF != null && autoSymmetryDSF.val) { bool isCenter = false; if (symMap != null && symMap.ContainsKey(idx)) { if (symMap[idx] == idx) isCenter = true; } else { Vector3 baseV = originalVertices[idx]; if (Mathf.Abs(baseV.x) < 0.0001f) isCenter = true; } if (isCenter) d.x = 0f; } int outIdx = genOnly ? (idx - bodyMax) : idx; if (!usedIds.Contains(outIdx)) { usedIds.Add(outIdx); deltaLines.Add(" [ " + outIdx + ", " + d.x.ToString("G9", ic) + ", " + d.y.ToString("G9", ic) + ", " + d.z.ToString("G9", ic) + " ]"); } // 镜像点 if (autoSymmetryDSF != null && autoSymmetryDSF.val && symMap != null) { int pairIdx; if (symMap.TryGetValue(idx, out pairIdx)) { if (pairIdx == idx) continue; int outPair = genOnly ? (pairIdx - bodyMax) : pairIdx; if (usedIds.Contains(outPair)) continue; usedIds.Add(outPair); Vector3 dMirror = new Vector3(-d.x, d.y, d.z); deltaLines.Add(" [ " + outPair + ", " + dMirror.x.ToString("G9", ic) + ", " + dMirror.y.ToString("G9", ic) + ", " + dMirror.z.ToString("G9", ic) + " ]"); } } } int count = deltaLines.Count; if (count == 0) { SuperController.LogError("No vertices have changed. count=0 (" + (genOnly ? "Gen" : "Body") + ")"); return; } string tag = genOnly ? "(Gen)" : "(Body)"; string genderTag = isMale ? "male" : "female"; string morphName = "Instant_" + System.DateTime.Now.ToString("yyyyMMdd_HHmm_ss") + "(" + genderTag + "_" + tag.Trim('(', ')') + ")"; string hex = "0x" + UnityEngine.Random.Range(0, 0x7FFFFFFF).ToString("x"); string parent = isMale ? (genOnly ? "/data/DAZ%203D/Genesis%202/Male/Anatomy/Genesis2MaleGenitalia.dsf#GenesisMaleGenitalia-1" : "/data/DAZ%203D/Genesis%202/Male/Genesis2Male.dsf#GenesisMale-1") : (genOnly ? "/data/DAZ%203D/Genesis%202/Female/Anatomy/Genesis2FemaleGenitalia.dsf#GenesisFemaleGenitalia-1" : "/data/DAZ%203D/Genesis%202/Female/Genesis2Female.dsf#GenesisFemale-1"); var sb = new System.Text.StringBuilder(4096); sb.AppendLine("{"); sb.AppendLine(" \"file_version\" : \"0.6.1.0\","); sb.AppendLine(" \"asset_info\" : {"); sb.AppendLine(" \"id\" : \"/data/DAZ%203D/Genesis%202/" + genderTag + "/Morphs/User/Product/" + morphName + ".dsf\","); sb.AppendLine(" \"type\" : \"modifier\","); sb.AppendLine(" \"contributor\" : { \"author\" : \"User\" },"); sb.AppendLine(" \"revision\" : \"0.0\","); sb.AppendLine(" \"modified\" : \"" + System.DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") + "\""); sb.AppendLine(" },"); sb.AppendLine(" \"modifier_library\" : ["); sb.AppendLine(" {"); sb.AppendLine(" \"id\" : \"" + morphName + "-" + hex + "\","); sb.AppendLine(" \"name\" : \"" + morphName + "\","); sb.AppendLine(" \"parent\" : \"" + parent + "\","); sb.AppendLine(" \"presentation\" : { \"type\" : \"Modifier/Shape\", \"label\" : \"" + morphName + "\" },"); sb.AppendLine(" \"channel\" : { \"id\":\"value\",\"type\":\"float\",\"name\":\"Value\",\"label\":\"" + morphName + "\",\"auto_follow\":true,\"value\":0,\"min\":-1,\"max\":1,\"clamped\":true,\"display_as_percent\":true,\"step_size\":0.01 },"); sb.AppendLine(" \"group\" : \"/Morphs/Morph Loader\","); sb.AppendLine(" \"morph\" : {"); sb.AppendLine(" \"vertex_count\" : " + vertexCount + ","); sb.AppendLine(" \"deltas\" : {"); sb.AppendLine(" \"count\" : " + count + ","); sb.AppendLine(" \"values\" : ["); for (int i = 0; i < deltaLines.Count; i++) { sb.Append(deltaLines[i]); if (i < deltaLines.Count - 1) sb.AppendLine(","); else sb.AppendLine(); } sb.AppendLine(" ]"); sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(" ],"); sb.AppendLine(" \"scene\" : { \"modifiers\" : [ { \"id\" : \"" + morphName + "\", \"url\" : \"#" + morphName + "-" + hex + "\" } ] }"); sb.AppendLine("}"); string dir = isMale ? (genOnly ? "Custom/Atom/Person/Morphs/male_genitalia" : "Custom/Atom/Person/Morphs/male") : (genOnly ? "Custom/Atom/Person/Morphs/female_genitalia" : "Custom/Atom/Person/Morphs/female"); if (!FileManagerSecure.DirectoryExists(dir)) FileManagerSecure.CreateDirectory(dir); string path = dir + "/" + morphName + ".dsf"; FileManagerSecure.WriteAllText(path, sb.ToString()); SuperController.LogMessage((genOnly ? "[Gen]" : "[Body]") + " DSF exported: " + path + " (gender=" + genderTag + ", count=" + count + ")"); return; } // ---- 拆分为左右两个 DSF 的逻辑 ---- else { List leftDeltaLines = new List(); List rightDeltaLines = new List(); HashSet usedLeft = new HashSet(); HashSet usedRight = new HashSet(); const float CENTER_EPS = 0.0001f; foreach (int idx in selectedVertices) { if (idx < 0 || idx >= maxLen) continue; if (genOnly && idx < bodyMax) continue; if (!genOnly && idx >= bodyMax) continue; Vector3 cur; GameObject go; if (vertexMarkers != null && vertexMarkers.TryGetValue(idx, out go) && go != null) cur = go.transform.localPosition; else cur = meshVertices[idx]; Vector3 orig = originalVertices[idx]; if (!IsFinite3(cur) || !IsFinite3(orig)) continue; Vector3 dispCur = cur - orig; if (!IsFinite3(dispCur) || dispCur.sqrMagnitude < EPS) continue; Vector3 dTP = invRotTP * dispCur; Vector3 d = new Vector3(-dTP.x, dTP.y, dTP.z) * 100f; bool isCenter = Mathf.Abs(orig.x) < CENTER_EPS; if (isCenter) { d.x = 0f; int outIdx = genOnly ? (idx - bodyMax) : idx; if (!usedLeft.Contains(outIdx)) { usedLeft.Add(outIdx); leftDeltaLines.Add(" [ " + outIdx + ", " + d.x.ToString("G9", ic) + ", " + d.y.ToString("G9", ic) + ", " + d.z.ToString("G9", ic) + " ]"); } if (!usedRight.Contains(outIdx)) { usedRight.Add(outIdx); rightDeltaLines.Add(" [ " + outIdx + ", " + d.x.ToString("G9", ic) + ", " + d.y.ToString("G9", ic) + ", " + d.z.ToString("G9", ic) + " ]"); } continue; } if (orig.x < 0f) { int outIdxLeft = genOnly ? (idx - bodyMax) : idx; if (!usedLeft.Contains(outIdxLeft)) { usedLeft.Add(outIdxLeft); leftDeltaLines.Add(" [ " + outIdxLeft + ", " + d.x.ToString("G9", ic) + ", " + d.y.ToString("G9", ic) + ", " + d.z.ToString("G9", ic) + " ]"); } if (symMap != null) { int pairIdx; if (symMap.TryGetValue(idx, out pairIdx) && pairIdx != idx) { int outIdxRight = genOnly ? (pairIdx - bodyMax) : pairIdx; if (!usedRight.Contains(outIdxRight)) { usedRight.Add(outIdxRight); Vector3 dMirror = new Vector3(-d.x, d.y, d.z); rightDeltaLines.Add(" [ " + outIdxRight + ", " + dMirror.x.ToString("G9", ic) + ", " + dMirror.y.ToString("G9", ic) + ", " + dMirror.z.ToString("G9", ic) + " ]"); } } } } else { int outIdxRight = genOnly ? (idx - bodyMax) : idx; if (!usedRight.Contains(outIdxRight)) { usedRight.Add(outIdxRight); rightDeltaLines.Add(" [ " + outIdxRight + ", " + d.x.ToString("G9", ic) + ", " + d.y.ToString("G9", ic) + ", " + d.z.ToString("G9", ic) + " ]"); } if (symMap != null) { int pairIdx; if (symMap.TryGetValue(idx, out pairIdx) && pairIdx != idx) { int outIdxLeft = genOnly ? (pairIdx - bodyMax) : pairIdx; if (!usedLeft.Contains(outIdxLeft)) { usedLeft.Add(outIdxLeft); Vector3 dMirror = new Vector3(-d.x, d.y, d.z); leftDeltaLines.Add(" [ " + outIdxLeft + ", " + dMirror.x.ToString("G9", ic) + ", " + dMirror.y.ToString("G9", ic) + ", " + dMirror.z.ToString("G9", ic) + " ]"); } } } } } string tagBase = genOnly ? "(Gen)" : "(Body)"; string genderTag = isMale ? "male" : "female"; string hex = "0x" + UnityEngine.Random.Range(0, 0x7FFFFFFF).ToString("x"); string dir = isMale ? (genOnly ? "Custom/Atom/Person/Morphs/male_genitalia" : "Custom/Atom/Person/Morphs/male") : (genOnly ? "Custom/Atom/Person/Morphs/female_genitalia" : "Custom/Atom/Person/Morphs/female"); if (!FileManagerSecure.DirectoryExists(dir)) FileManagerSecure.CreateDirectory(dir); // helper 写文件 System.Action, string> WriteDsf = (deltaList, sideLabel) => { if (deltaList == null || deltaList.Count == 0) return; string morphNameSide = "Instant_" + System.DateTime.Now.ToString("yyyyMMdd_HHmm_ss") + "(" + genderTag + "_" + tagBase.Trim('(', ')') + "-" + sideLabel + ")"; var sbSide = new System.Text.StringBuilder(4096); sbSide.AppendLine("{"); sbSide.AppendLine(" \"file_version\" : \"0.6.1.0\","); sbSide.AppendLine(" \"asset_info\" : {"); sbSide.AppendLine(" \"id\" : \"/data/DAZ%203D/Genesis%202/" + genderTag + "/Morphs/User/Product/" + morphNameSide + ".dsf\","); sbSide.AppendLine(" \"type\" : \"modifier\","); sbSide.AppendLine(" \"contributor\" : { \"author\" : \"User\" },"); sbSide.AppendLine(" \"revision\" : \"0.0\","); sbSide.AppendLine(" \"modified\" : \"" + System.DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") + "\""); sbSide.AppendLine(" },"); sbSide.AppendLine(" \"modifier_library\" : ["); sbSide.AppendLine(" {"); sbSide.AppendLine(" \"id\" : \"" + morphNameSide + "-" + hex + "\","); sbSide.AppendLine(" \"name\" : \"" + morphNameSide + "\","); string parentSide = isMale ? (genOnly ? "/data/DAZ%203D/Genesis%202/Male/Anatomy/Genesis2MaleGenitalia.dsf#GenesisMaleGenitalia-1" : "/data/DAZ%203D/Genesis%202/Male/Genesis2Male.dsf#GenesisMale-1") : (genOnly ? "/data/DAZ%203D/Genesis%202/Female/Anatomy/Genesis2FemaleGenitalia.dsf#GenesisFemaleGenitalia-1" : "/data/DAZ%203D/Genesis%202/Female/Genesis2Female.dsf#GenesisFemale-1"); sbSide.AppendLine(" \"parent\" : \"" + parentSide + "\","); sbSide.AppendLine(" \"presentation\" : { \"type\" : \"Modifier/Shape\", \"label\" : \"" + morphNameSide + "\" },"); sbSide.AppendLine(" \"channel\" : { \"id\":\"value\",\"type\":\"float\",\"name\":\"Value\",\"label\":\"" + morphNameSide + "\",\"auto_follow\":true,\"value\":0,\"min\":-1,\"max\":1,\"clamped\":true,\"display_as_percent\":true,\"step_size\":0.01 },"); sbSide.AppendLine(" \"group\" : \"/Morphs/Morph Loader\","); sbSide.AppendLine(" \"morph\" : {"); sbSide.AppendLine(" \"vertex_count\" : " + vertexCount + ","); sbSide.AppendLine(" \"deltas\" : {"); sbSide.AppendLine(" \"count\" : " + deltaList.Count + ","); sbSide.AppendLine(" \"values\" : ["); for (int i = 0; i < deltaList.Count; i++) { sbSide.Append(deltaList[i]); if (i < deltaList.Count - 1) sbSide.AppendLine(","); else sbSide.AppendLine(); } sbSide.AppendLine(" ]"); sbSide.AppendLine(" }"); sbSide.AppendLine(" }"); sbSide.AppendLine(" }"); sbSide.AppendLine(" ],"); sbSide.AppendLine(" \"scene\" : { \"modifiers\" : [ { \"id\" : \"" + morphNameSide + "\", \"url\" : \"#" + morphNameSide + "-" + hex + "\" } ] }"); sbSide.AppendLine("}"); string pathSide = dir + "/" + morphNameSide + ".dsf"; FileManagerSecure.WriteAllText(pathSide, sbSide.ToString()); SuperController.LogMessage("[Split DSF] " + sideLabel + " DSF exported: " + pathSide + " (gender=" + genderTag + ", count=" + deltaList.Count + ")"); }; WriteDsf(leftDeltaLines, "Left"); WriteDsf(rightDeltaLines, "Right"); if ((leftDeltaLines == null || leftDeltaLines.Count == 0) && (rightDeltaLines == null || rightDeltaLines.Count == 0)) { SuperController.LogError("Split symmetry DSF selected but no deltas found for either side."); } return; } } catch (System.Exception ex) { SuperController.LogError("ExportMorphDSF_Internal ERROR: " + ex.Message); } } ///////////////////////////////////////////////////////////////////// private void MirrorSelectedVertices() { BuildSymMaps(); bool genOnly = (genSelectOnly != null && genSelectOnly.val); Dictionary symMapLocal = genOnly ? symMapGen : symMapBody; if (symMapLocal == null || symMapLocal.Count == 0) { SuperController.LogMessage("Symmetry map not built."); return; } if (dragDelta == null || dragDelta.Length != originalVertices.Length) dragDelta = new Vector3[originalVertices.Length]; if (sharedBlueMat == null) { sharedBlueMat = new Material(Shader.Find("Battlehub/RTGizmos/Handles")); sharedBlueMat.color = Color.blue; sharedBlueMat.renderQueue = 3100; } if (sharedGreenMat == null) { sharedGreenMat = new Material(Shader.Find("Battlehub/RTGizmos/Handles")); sharedGreenMat.color = Color.green; sharedGreenMat.renderQueue = 3200; } List snap = new List(selectedVertices); foreach (int vid in snap) { GameObject go; if (vertexMarkers.TryGetValue(vid, out go) && go != null) UnityEngine.Object.Destroy(go); vertexMarkers.Remove(vid); } selectedVertices.Clear(); int mirrored = 0, centered = 0; foreach (int i in snap) { int j; bool hasPair = symMapLocal.TryGetValue(i, out j); if (!hasPair) { GameObject mkBlue; if (!baseMarkers.TryGetValue(i, out mkBlue) || mkBlue == null) { mkBlue = GameObject.CreatePrimitive(PrimitiveType.Quad); UnityEngine.Object.DestroyImmediate(mkBlue.GetComponent()); mkBlue.layer = LayerMask.NameToLayer("Ignore Raycast"); mkBlue.transform.SetParent(markerRoot.transform, false); mkBlue.GetComponent().sharedMaterial = sharedBlueMat; baseMarkers[i] = mkBlue; } mkBlue.transform.localScale = Vector3.one * 0.0002f; // ★再設定 mkBlue.transform.localPosition = originalVertices[i]; CreateSelectionMarker(i); GameObject c; if (vertexMarkers.TryGetValue(i, out c) && c != null) { c.transform.localScale = Vector3.one * 0.0001f; // ★再設定 c.transform.localPosition = originalVertices[i] + dragDelta[i]; } selectedVertices.Add(i); centered++; continue; } if (i < 0 || j < 0 || i >= originalVertices.Length || j >= originalVertices.Length) continue; Vector3 delta = dragDelta[i]; Vector3 deltaMirror = new Vector3(-delta.x, delta.y, delta.z); dragDelta[j] = deltaMirror; Vector3 dstPos = originalVertices[j] + deltaMirror; GameObject mkBlue2; if (!baseMarkers.TryGetValue(j, out mkBlue2) || mkBlue2 == null) { mkBlue2 = GameObject.CreatePrimitive(PrimitiveType.Quad); UnityEngine.Object.DestroyImmediate(mkBlue2.GetComponent()); mkBlue2.layer = LayerMask.NameToLayer("Ignore Raycast"); mkBlue2.transform.SetParent(markerRoot.transform, false); mkBlue2.GetComponent().sharedMaterial = sharedBlueMat; baseMarkers[j] = mkBlue2; } mkBlue2.transform.localScale = Vector3.one * 0.0002f; // ★再設定 mkBlue2.transform.localPosition = originalVertices[j]; CreateSelectionMarker(j); GameObject newGo; if (vertexMarkers.TryGetValue(j, out newGo) && newGo != null) { newGo.transform.localScale = Vector3.one * 0.0001f; // ★再設定 newGo.transform.localPosition = dstPos; } selectedVertices.Add(j); mirrored++; } foreach (var kv in baseMarkers.ToList()) { if (!selectedVertices.Contains(kv.Key)) { if (kv.Value != null) UnityEngine.Object.Destroy(kv.Value); baseMarkers.Remove(kv.Key); } } //SuperController.LogMessage($"Symmetry mirrored {mirrored} vertices, kept {centered} center vertices."); ApplyXYOffsetsToSelected(); //originalVerticesJSON.setCallbackFunction(true); } ///////////////////////////////////////////////////////////// // === 補助:最も近い頂点IDを返す === int FindNearestVertex(Vector3 localHit) { float bestDist = float.MaxValue; int nearest = -1; for (int i = 0; i < meshVertices.Length; i++) { float d = (meshVertices[i] - localHit).sqrMagnitude; if (d < bestDist) { bestDist = d; nearest = i; } } return nearest; } //////////////////////////////////////////////////////// ///////////////////////////////////// // clear everything about selection, without destroying objects // 選択解除+完全リセット(頂点オフセットも原点に戻す) private void ClearAllSelection() { // === 1) 選択頂点を original に戻す === if (selectedVertices != null && selectedVertices.Count > 0) { if (dragDelta == null || dragDelta.Length != originalVertices.Length) dragDelta = new Vector3[originalVertices.Length]; foreach (int idx in selectedVertices) { meshVertices[idx] = originalVertices[idx]; dragDelta[idx] = Vector3.zero; GameObject go; if (vertexMarkers != null && vertexMarkers.TryGetValue(idx, out go) && go != null) go.transform.localPosition = originalVertices[idx]; GameObject bo; if (baseMarkers != null && baseMarkers.TryGetValue(idx, out bo) && bo != null) bo.transform.localPosition = originalVertices[idx]; } } // === 2) スライダー・内部値リセット === _xOffset = _yOffset = _zOffset = 0f; _sxVal = _syVal = _szVal = 1f; _xRot = _yRot = _zRot = 0f; if (xOffsetSlider != null) xOffsetSlider.valNoCallback = 0f; if (yOffsetSlider != null) yOffsetSlider.valNoCallback = 0f; if (zOffsetSlider != null) zOffsetSlider.valNoCallback = 0f; if (xScaleSlider != null) xScaleSlider.valNoCallback = 1f; if (yScaleSlider != null) yScaleSlider.valNoCallback = 1f; if (zScaleSlider != null) zScaleSlider.valNoCallback = 1f; if (xRotateSlider != null) xRotateSlider.valNoCallback = 0f; if (yRotateSlider != null) yRotateSlider.valNoCallback = 0f; if (zRotateSlider != null) zRotateSlider.valNoCallback = 0f; // === 3) 可視オブジェクトを完全破棄 === if (selectionSurface != null) { UnityEngine.Object.DestroyImmediate(selectionSurface); selectionSurface = null; } if (edgeLines != null) { foreach (var lr in edgeLines) if (lr != null) UnityEngine.Object.DestroyImmediate(lr.gameObject); edgeLines.Clear(); } if (vertexMarkers != null) { foreach (var kv in vertexMarkers) if (kv.Value != null) UnityEngine.Object.DestroyImmediate(kv.Value); vertexMarkers.Clear(); } if (baseMarkers != null) { foreach (var kv in baseMarkers) if (kv.Value != null) UnityEngine.Object.DestroyImmediate(kv.Value); baseMarkers.Clear(); } // === 4) 選択集合クリア === selectedVertices?.Clear(); // === 5) マーカ親を再生成 === if (markerRoot != null) UnityEngine.Object.DestroyImmediate(markerRoot); markerRoot = CreateChildObject(containingAtom.gameObject, "VertexMarkers"); markerRoot.layer = LayerMask.NameToLayer("Ignore Raycast"); // === 6) hover/painter 再構築 === hoverMarker = CreateMarker(Color.yellow, 0.0005f); hoverMarker.name = "HoverMarker"; hoverMarker.transform.SetParent(markerRoot.transform, false); hoverMarker.SetActive(false); paintCircle = CreateRingMarker(Color.red, 0.01f, 0.0006f, 64); paintCircle.name = "PaintCircle"; paintCircle.transform.SetParent(markerRoot.transform, false); paintCircle.SetActive(false); // === 7) 配列の整合 === if (dragDelta == null || dragDelta.Length != originalVertices.Length) dragDelta = new Vector3[originalVertices.Length]; //SuperController.LogMessage("ClearAllSelection: all markers destroyed immediately, visuals cleared."); } ////////////////////////////////////////////////////////////////// // 追加 private Vector3 CalcBluePos(int idx) { Vector3 orig = originalVertices[idx]; // ★左右同方向(フリップしない) return new Vector3( orig.x + _xOffset, orig.y + _yOffset, orig.z + _zOffset ); } /////////////////////////////////////////////////////////////////// // これで統一:オフセットとスケールをまとめて反映 private void ApplyXYOffsetsToSelected() { if (selectedVertices == null || selectedVertices.Count == 0) return; if (dragDelta == null || dragDelta.Length != originalVertices.Length) dragDelta = new Vector3[originalVertices.Length]; // pivot(original基準) float pivotX = 0f, pivotY = 0f, pivotZ = 0f; int n = 0; foreach (int i in selectedVertices) { if (i < 0 || i >= originalVertices.Length) continue; Vector3 o = originalVertices[i]; pivotX += o.x; pivotY += o.y; pivotZ += o.z; n++; } if (n > 0) { pivotX /= n; pivotY /= n; pivotZ /= n; } Vector3 pivot = new Vector3(pivotX, pivotY, pivotZ); // 回転クォータニオン Quaternion qx = Quaternion.AngleAxis(_xRot, Vector3.right); Quaternion qy = Quaternion.AngleAxis(_yRot, Vector3.up); Quaternion qz = Quaternion.AngleAxis(_zRot, Vector3.forward); Quaternion rot = qz * qy * qx; foreach (int idx in selectedVertices) { GameObject go; if (!vertexMarkers.TryGetValue(idx, out go) || go == null) continue; Vector3 orig = originalVertices[idx]; // pivot基準スケール float sx = pivot.x + (orig.x - pivot.x) * _sxVal; float sy = pivot.y + (orig.y - pivot.y) * _syVal; float sz = pivot.z + (orig.z - pivot.z) * _szVal; Vector3 scaled = new Vector3(sx, sy, sz); // pivot基準で回転 Vector3 p = scaled - pivot; p = rot * p; p += pivot; // オフセット+ドラッグ分 Vector3 newPos = p + new Vector3(_xOffset, _yOffset, _zOffset) + dragDelta[idx]; go.transform.localPosition = newPos; meshVertices[idx] = newPos; } BuildSelectionSurface(); } /////////////////////////////////////// void LogCenterLineVertices() { if (meshVertices == null || meshVertices.Length == 0) { SuperController.LogMessage("meshVertices not init"); return; } List centerLineVerts = new List(); for (int i = 0; i < meshVertices.Length; i++) { if (Mathf.Abs(meshVertices[i].x) < SYM_EPS) centerLineVerts.Add(i); } SuperController.LogMessage($"中心線の頂点数: {centerLineVerts.Count}"); for (int j = 0; j < centerLineVerts.Count; j += 10) // ← ここを j に変更 { int len = Math.Min(10, centerLineVerts.Count - j); List slice = centerLineVerts.GetRange(j, len); SuperController.LogMessage("Verts: " + string.Join(", ", slice.ConvertAll(k => k.ToString()).ToArray())); } } ///////////////////////////////////////// private void FilterMorphs(string keyword) { matchedMorphs.Clear(); if (string.IsNullOrEmpty(keyword) || keyword.Length < 3) { SuperController.LogMessage("Type at least 3 characters to search morphs."); return; } var person = containingAtom?.GetComponentInChildren(); if (person == null || person.morphsControlUI == null) return; int gender = GetGenderInt(containingAtom); // 0=female, 1=male foreach (var m in person.morphsControlUI.GetMorphs()) { if (m.displayName.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) < 0) continue; if (gender == 0) { // female の場合 male系を除外 if (m.region != null && m.region.ToLower().Contains("male")) continue; } else if (gender == 1) { // male の場合 female系を除外 if (m.region != null && m.region.ToLower().Contains("female")) continue; } matchedMorphs.Add(m.displayName); } //if (matchedMorphs.Count == 0) // SuperController.LogMessage("No morphs matched: \"" + keyword + "\""); //else // SuperController.LogMessage("Matched morphs (" + matchedMorphs.Count + "): " + string.Join(", ", matchedMorphs)); } ///////////////////////////////////////// private void BuildMorphSliders() { // 既存スライダーを削除 foreach (var s in morphSliders) RemoveSlider(s); morphSliders.Clear(); var person = containingAtom?.GetComponentInChildren(); if (person == null || person.morphsControlUI == null) return; // 逆順で処理 for (int i = matchedMorphs.Count - 1; i >= 0; i--) { var morphName = matchedMorphs[i]; var m = person.morphsControlUI.GetMorphByDisplayName(morphName); if (m == null) continue; // --- スライダー生成 --- var jsf = new JSONStorableFloat(m.displayName, 0f, -_globalMax, _globalMax, true); jsf.setCallbackFunction = (float v) => { m.morphValue = v; }; RegisterFloat(jsf); var slider = CreateSlider(jsf, true); morphSliders.Add(slider); jsf.valNoCallback = m.morphValue; // --- body/gen 判定 --- bool isGen = false; if (!string.IsNullOrEmpty(m.region)) { isGen = m.region.IndexOf("Genital", StringComparison.OrdinalIgnoreCase) >= 0; } else { // region が空なら名前で推定 string name = m.displayName.ToLower(); isGen = name.Contains("gen"); } // --- ラベル色変更 --- var label = slider.transform.Find("Text")?.GetComponent(); if (label != null) { string lowerName = morphName.ToLower(); if (lowerName.Contains("gen")) label.color = new Color(0.8f, 0.2f, 0.4f, 1f); // gen含む → ピンク else label.color = new Color(0f, 0.3f, 0.8f, 1f); // body → 青系 } } SuperController.LogMessage("Created " + morphSliders.Count + " morph sliders."); } //////////////////////////////////////// private void SetAllFreeControllersCanGrab(bool allow) { try { if (containingAtom == null) return; var fcs = containingAtom.freeControllers; if (fcs == null) return; foreach (var fc in fcs) { if (fc == null) continue; fc.canGrabPosition = allow; } } catch (Exception) { /* 静かに失敗させる */ } } /////////////////////////////////////// GameObject selectionSurface; List edgeLines = new List(); // === OPT: Throttled wrapper === void BuildSelectionSurfaceThrottled() { if (Time.time - __lastSurfaceBuildTime < __SURFACE_BUILD_INTERVAL) return; __lastSurfaceBuildTime = Time.time; BuildSelectionSurface(); } //////////////////////////////////////// void BuildSelectionSurface() { PurgeInvalidSelections(); if (meshTriangles == null || selectedVertices == null || selectedVertices.Count == 0) { if (selectionSurface != null) selectionSurface.SetActive(false); foreach (var lr in edgeLines) if (lr != null) lr.gameObject.SetActive(false); return; } if (cachedVerts == null || cachedVerts.Length != meshVertices.Length) cachedVerts = new Vector3[meshVertices.Length]; Vector3[] currentVerts = cachedVerts; // 緑(優先)→青→mesh の順で座標を反映 for (int i = 0; i < meshVertices.Length; i++) { GameObject g; if (vertexMarkers != null && vertexMarkers.TryGetValue(i, out g) && g != null) currentVerts[i] = g.transform.localPosition; else if (baseMarkers != null && baseMarkers.TryGetValue(i, out g) && g != null && g.activeInHierarchy) currentVerts[i] = g.transform.localPosition; else currentVerts[i] = meshVertices[i]; } // 選択三角形を抽出 List selTris = new List(); for (int i = 0; i < meshTriangles.Length; i += 3) { int a = meshTriangles[i], b = meshTriangles[i + 1], c = meshTriangles[i + 2]; if (selectedVertices.Contains(a) && selectedVertices.Contains(b) && selectedVertices.Contains(c)) { selTris.Add(a); selTris.Add(b); selTris.Add(c); } } if (selTris.Count == 0) { if (selectionSurface != null) selectionSurface.SetActive(false); foreach (var lr in edgeLines) if (lr != null) lr.gameObject.SetActive(false); return; } // === 再描画: 既存ラインを破棄して再生成 === if (edgeLines != null && edgeLines.Count > 0) { foreach (var lr in edgeLines) if (lr != null) UnityEngine.Object.Destroy(lr.gameObject); edgeLines.Clear(); } // 新規稜線を構築 for (int i = 0; i < selTris.Count; i += 3) { int a = selTris[i], b = selTris[i + 1], c = selTris[i + 2]; AddEdgeLine(currentVerts[a], currentVerts[b]); AddEdgeLine(currentVerts[b], currentVerts[c]); AddEdgeLine(currentVerts[c], currentVerts[a]); } } /////////////////////////////////////////////////////////////////////////////// void AddEdgeLine(Vector3 p1, Vector3 p2) { var cam = Camera.main; if (cam == null) return; // カメラ方向へ微小オフセット(0.001m) Vector3 offset = cam.transform.forward * -0.0005f; p1 += offset; p2 += offset; // --- 再利用構造 --- // edgeLines の中に未使用の LineRenderer があれば使い回す LineRenderer lr = null; foreach (var l in edgeLines) { if (l != null && !l.gameObject.activeSelf) { lr = l; break; } } // 未使用が無ければ新規作成 if (lr == null) { var go = new GameObject("EdgeLine"); go.layer = LayerMask.NameToLayer("Ignore Raycast"); go.transform.SetParent(markerRoot.transform, false); lr = go.AddComponent(); lr.positionCount = 2; lr.startWidth = lr.endWidth = 0.0002f; lr.useWorldSpace = true; if (sharedEdgeMat == null) { sharedEdgeMat = new Material(Shader.Find("Sprites/Default")); sharedEdgeMat.color = Color.black; sharedEdgeMat.SetInt("_ZWrite", 0); } lr.material = sharedEdgeMat; edgeLines.Add(lr); } // --- 再設定 --- lr.gameObject.SetActive(true); lr.SetPosition(0, p1); lr.SetPosition(1, p2); } ////////////////////////////////////////////////////// // 選択メッシュに対してラプラシアンスムーズを数回かける private void SmoothPreview(Mesh mesh, int iterations = 2) { if (mesh == null) return; var verts = mesh.vertices; var tris = mesh.triangles; // 隣接リスト作成 List[] neighbors = new List[verts.Length]; for (int i = 0; i < verts.Length; i++) neighbors[i] = new List(); for (int i = 0; i < tris.Length; i += 3) { int a = tris[i], b = tris[i + 1], c = tris[i + 2]; if (!neighbors[a].Contains(b)) neighbors[a].Add(b); if (!neighbors[a].Contains(c)) neighbors[a].Add(c); if (!neighbors[b].Contains(a)) neighbors[b].Add(a); if (!neighbors[b].Contains(c)) neighbors[b].Add(c); if (!neighbors[c].Contains(a)) neighbors[c].Add(a); if (!neighbors[c].Contains(b)) neighbors[c].Add(b); } for (int it = 0; it < iterations; it++) { Vector3[] newVerts = new Vector3[verts.Length]; for (int i = 0; i < verts.Length; i++) { if (neighbors[i].Count == 0) { newVerts[i] = verts[i]; continue; } Vector3 avg = Vector3.zero; foreach (int n in neighbors[i]) avg += verts[n]; avg /= neighbors[i].Count; newVerts[i] = Vector3.Lerp(verts[i], avg, 0.5f); // 0.5で滑らかに } verts = newVerts; } mesh.vertices = verts; mesh.RecalculateNormals(); } ////////////////////////////////////////////////////////// /* private void CreateImageButton(float x, float y, string imageFilename, Action callback) { // 生の GameObject を直接生成 var go = new GameObject("MiniImageBtn", typeof(RectTransform), typeof(RawImage), typeof(Button)); go.transform.SetParent(UITransform, false); var rt = go.GetComponent(); rt.sizeDelta = new Vector2(48f, 48f); rt.anchoredPosition = new Vector2(x, y); // 自由配置が効く // 画像設定 var img = go.GetComponent(); var tex = LoadTex(imageFilename); if (tex != null) img.texture = tex; img.color = Color.white; // ボタン処理 var btn = go.GetComponent