ugui 소스 제거
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 468ba7fb3e976344eb340d05295e56e6
|
|
||||||
PrefabImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
67
Assets/Resources/SHI/Prefabs/ISOPModal.prefab
Normal file
67
Assets/Resources/SHI/Prefabs/ISOPModal.prefab
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!1 &3129090381825983309
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 1951183495053314226}
|
||||||
|
- component: {fileID: 5150788519260695383}
|
||||||
|
- component: {fileID: 5178822619495094378}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: ISOPModal
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!4 &1951183495053314226
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 3129090381825983309}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!114 &5150788519260695383
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 3129090381825983309}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 19102, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier:
|
||||||
|
m_PanelSettings: {fileID: 11400000, guid: 23c44a2b28cd90c418bdfb0ea56f91e8, type: 2}
|
||||||
|
m_ParentUI: {fileID: 0}
|
||||||
|
sourceAsset: {fileID: 9197481963319205126, guid: c64f687137f7a2940861d34fc593a917, type: 3}
|
||||||
|
m_SortingOrder: 100
|
||||||
|
m_WorldSpaceSizeMode: 1
|
||||||
|
m_WorldSpaceWidth: 1920
|
||||||
|
m_WorldSpaceHeight: 1080
|
||||||
|
--- !u!114 &5178822619495094378
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 3129090381825983309}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: a070d33d51211a84ea9e6c6e6133735f, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier:
|
||||||
|
uiDocument: {fileID: 5150788519260695383}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 2a2c49ea5939c5749aa88e892518f979
|
guid: 7382f025177c6264da13b7255d6480d2
|
||||||
PrefabImporter:
|
PrefabImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
userData:
|
userData:
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: c6c35cdcefd487f4b910ceed76b50a8f
|
|
||||||
PrefabImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,823 +0,0 @@
|
|||||||
%YAML 1.1
|
|
||||||
%TAG !u! tag:unity3d.com,2011:
|
|
||||||
--- !u!1 &1462360020605571893
|
|
||||||
GameObject:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
serializedVersion: 6
|
|
||||||
m_Component:
|
|
||||||
- component: {fileID: 1022765863909309765}
|
|
||||||
- component: {fileID: 2951520196524031467}
|
|
||||||
- component: {fileID: 2289736159566368719}
|
|
||||||
- component: {fileID: 6025781672912143612}
|
|
||||||
m_Layer: 5
|
|
||||||
m_Name: Scrollbar Horizontal
|
|
||||||
m_TagString: Untagged
|
|
||||||
m_Icon: {fileID: 0}
|
|
||||||
m_NavMeshLayer: 0
|
|
||||||
m_StaticEditorFlags: 0
|
|
||||||
m_IsActive: 1
|
|
||||||
--- !u!224 &1022765863909309765
|
|
||||||
RectTransform:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1462360020605571893}
|
|
||||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
|
||||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
|
||||||
m_ConstrainProportionsScale: 0
|
|
||||||
m_Children:
|
|
||||||
- {fileID: 3097322909638401089}
|
|
||||||
m_Father: {fileID: 8894486403032071402}
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
|
||||||
m_AnchorMin: {x: 0, y: 0}
|
|
||||||
m_AnchorMax: {x: 1, y: 0}
|
|
||||||
m_AnchoredPosition: {x: 0, y: 0}
|
|
||||||
m_SizeDelta: {x: 0, y: 5}
|
|
||||||
m_Pivot: {x: 0, y: 0}
|
|
||||||
--- !u!222 &2951520196524031467
|
|
||||||
CanvasRenderer:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1462360020605571893}
|
|
||||||
m_CullTransparentMesh: 1
|
|
||||||
--- !u!114 &2289736159566368719
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1462360020605571893}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_Material: {fileID: 0}
|
|
||||||
m_Color: {r: 0.09411765, g: 0.09411765, b: 0.09411765, a: 1}
|
|
||||||
m_RaycastTarget: 1
|
|
||||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
|
||||||
m_Maskable: 1
|
|
||||||
m_OnCullStateChanged:
|
|
||||||
m_PersistentCalls:
|
|
||||||
m_Calls: []
|
|
||||||
m_Sprite: {fileID: 0}
|
|
||||||
m_Type: 1
|
|
||||||
m_PreserveAspect: 0
|
|
||||||
m_FillCenter: 1
|
|
||||||
m_FillMethod: 4
|
|
||||||
m_FillAmount: 1
|
|
||||||
m_FillClockwise: 1
|
|
||||||
m_FillOrigin: 0
|
|
||||||
m_UseSpriteMesh: 0
|
|
||||||
m_PixelsPerUnitMultiplier: 1
|
|
||||||
--- !u!114 &6025781672912143612
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1462360020605571893}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: 2a4db7a114972834c8e4117be1d82ba3, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_Navigation:
|
|
||||||
m_Mode: 3
|
|
||||||
m_WrapAround: 0
|
|
||||||
m_SelectOnUp: {fileID: 0}
|
|
||||||
m_SelectOnDown: {fileID: 0}
|
|
||||||
m_SelectOnLeft: {fileID: 0}
|
|
||||||
m_SelectOnRight: {fileID: 0}
|
|
||||||
m_Transition: 1
|
|
||||||
m_Colors:
|
|
||||||
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
|
|
||||||
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
|
||||||
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
|
|
||||||
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
|
||||||
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
|
|
||||||
m_ColorMultiplier: 1
|
|
||||||
m_FadeDuration: 0.1
|
|
||||||
m_SpriteState:
|
|
||||||
m_HighlightedSprite: {fileID: 0}
|
|
||||||
m_PressedSprite: {fileID: 0}
|
|
||||||
m_SelectedSprite: {fileID: 0}
|
|
||||||
m_DisabledSprite: {fileID: 0}
|
|
||||||
m_AnimationTriggers:
|
|
||||||
m_NormalTrigger: Normal
|
|
||||||
m_HighlightedTrigger: Highlighted
|
|
||||||
m_PressedTrigger: Pressed
|
|
||||||
m_SelectedTrigger: Selected
|
|
||||||
m_DisabledTrigger: Disabled
|
|
||||||
m_Interactable: 1
|
|
||||||
m_TargetGraphic: {fileID: 2680734087760533677}
|
|
||||||
m_HandleRect: {fileID: 6081086258189437538}
|
|
||||||
m_Direction: 0
|
|
||||||
m_Value: 0
|
|
||||||
m_Size: 1
|
|
||||||
m_NumberOfSteps: 0
|
|
||||||
m_OnValueChanged:
|
|
||||||
m_PersistentCalls:
|
|
||||||
m_Calls: []
|
|
||||||
--- !u!1 &1572880287690542826
|
|
||||||
GameObject:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
serializedVersion: 6
|
|
||||||
m_Component:
|
|
||||||
- component: {fileID: 3301928374361144381}
|
|
||||||
m_Layer: 5
|
|
||||||
m_Name: Sliding Area
|
|
||||||
m_TagString: Untagged
|
|
||||||
m_Icon: {fileID: 0}
|
|
||||||
m_NavMeshLayer: 0
|
|
||||||
m_StaticEditorFlags: 0
|
|
||||||
m_IsActive: 1
|
|
||||||
--- !u!224 &3301928374361144381
|
|
||||||
RectTransform:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1572880287690542826}
|
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
|
||||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
|
||||||
m_ConstrainProportionsScale: 0
|
|
||||||
m_Children:
|
|
||||||
- {fileID: 6234462523872176539}
|
|
||||||
m_Father: {fileID: 5780838202259870402}
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
|
||||||
m_AnchorMin: {x: 0, y: 0}
|
|
||||||
m_AnchorMax: {x: 1, y: 1}
|
|
||||||
m_AnchoredPosition: {x: 0, y: 0}
|
|
||||||
m_SizeDelta: {x: -20, y: -20}
|
|
||||||
m_Pivot: {x: 0.5, y: 0.5}
|
|
||||||
--- !u!1 &1848783212353540151
|
|
||||||
GameObject:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
serializedVersion: 6
|
|
||||||
m_Component:
|
|
||||||
- component: {fileID: 5780838202259870402}
|
|
||||||
- component: {fileID: 2689031090661273458}
|
|
||||||
- component: {fileID: 4121319568877025449}
|
|
||||||
- component: {fileID: 4856300785373777908}
|
|
||||||
m_Layer: 5
|
|
||||||
m_Name: Scrollbar Vertical
|
|
||||||
m_TagString: Untagged
|
|
||||||
m_Icon: {fileID: 0}
|
|
||||||
m_NavMeshLayer: 0
|
|
||||||
m_StaticEditorFlags: 0
|
|
||||||
m_IsActive: 1
|
|
||||||
--- !u!224 &5780838202259870402
|
|
||||||
RectTransform:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1848783212353540151}
|
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
|
||||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
|
||||||
m_ConstrainProportionsScale: 0
|
|
||||||
m_Children:
|
|
||||||
- {fileID: 3301928374361144381}
|
|
||||||
m_Father: {fileID: 8894486403032071402}
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
|
||||||
m_AnchorMin: {x: 1, y: 0}
|
|
||||||
m_AnchorMax: {x: 1, y: 1}
|
|
||||||
m_AnchoredPosition: {x: 0, y: 0}
|
|
||||||
m_SizeDelta: {x: 5, y: 0}
|
|
||||||
m_Pivot: {x: 1, y: 1}
|
|
||||||
--- !u!222 &2689031090661273458
|
|
||||||
CanvasRenderer:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1848783212353540151}
|
|
||||||
m_CullTransparentMesh: 0
|
|
||||||
--- !u!114 &4121319568877025449
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1848783212353540151}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_Material: {fileID: 0}
|
|
||||||
m_Color: {r: 0.09411765, g: 0.09411765, b: 0.09411765, a: 1}
|
|
||||||
m_RaycastTarget: 1
|
|
||||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
|
||||||
m_Maskable: 1
|
|
||||||
m_OnCullStateChanged:
|
|
||||||
m_PersistentCalls:
|
|
||||||
m_Calls: []
|
|
||||||
m_Sprite: {fileID: 0}
|
|
||||||
m_Type: 1
|
|
||||||
m_PreserveAspect: 0
|
|
||||||
m_FillCenter: 1
|
|
||||||
m_FillMethod: 4
|
|
||||||
m_FillAmount: 1
|
|
||||||
m_FillClockwise: 1
|
|
||||||
m_FillOrigin: 0
|
|
||||||
m_UseSpriteMesh: 0
|
|
||||||
m_PixelsPerUnitMultiplier: 1
|
|
||||||
--- !u!114 &4856300785373777908
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1848783212353540151}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: 2a4db7a114972834c8e4117be1d82ba3, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_Navigation:
|
|
||||||
m_Mode: 3
|
|
||||||
m_WrapAround: 0
|
|
||||||
m_SelectOnUp: {fileID: 0}
|
|
||||||
m_SelectOnDown: {fileID: 0}
|
|
||||||
m_SelectOnLeft: {fileID: 0}
|
|
||||||
m_SelectOnRight: {fileID: 0}
|
|
||||||
m_Transition: 1
|
|
||||||
m_Colors:
|
|
||||||
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
|
|
||||||
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
|
||||||
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
|
|
||||||
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
|
|
||||||
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
|
|
||||||
m_ColorMultiplier: 1
|
|
||||||
m_FadeDuration: 0.1
|
|
||||||
m_SpriteState:
|
|
||||||
m_HighlightedSprite: {fileID: 0}
|
|
||||||
m_PressedSprite: {fileID: 0}
|
|
||||||
m_SelectedSprite: {fileID: 0}
|
|
||||||
m_DisabledSprite: {fileID: 0}
|
|
||||||
m_AnimationTriggers:
|
|
||||||
m_NormalTrigger: Normal
|
|
||||||
m_HighlightedTrigger: Highlighted
|
|
||||||
m_PressedTrigger: Pressed
|
|
||||||
m_SelectedTrigger: Highlighted
|
|
||||||
m_DisabledTrigger: Disabled
|
|
||||||
m_Interactable: 1
|
|
||||||
m_TargetGraphic: {fileID: 7428146289359687929}
|
|
||||||
m_HandleRect: {fileID: 6234462523872176539}
|
|
||||||
m_Direction: 2
|
|
||||||
m_Value: 0
|
|
||||||
m_Size: 1
|
|
||||||
m_NumberOfSteps: 0
|
|
||||||
m_OnValueChanged:
|
|
||||||
m_PersistentCalls:
|
|
||||||
m_Calls: []
|
|
||||||
--- !u!1 &3011982018749398316
|
|
||||||
GameObject:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
serializedVersion: 6
|
|
||||||
m_Component:
|
|
||||||
- component: {fileID: 8894486403032071402}
|
|
||||||
- component: {fileID: 9000987459549261387}
|
|
||||||
- component: {fileID: 4415661564241239138}
|
|
||||||
- component: {fileID: 2006518343008415563}
|
|
||||||
- component: {fileID: 5742779717166474465}
|
|
||||||
m_Layer: 5
|
|
||||||
m_Name: ShiTreeList
|
|
||||||
m_TagString: Untagged
|
|
||||||
m_Icon: {fileID: 0}
|
|
||||||
m_NavMeshLayer: 0
|
|
||||||
m_StaticEditorFlags: 0
|
|
||||||
m_IsActive: 1
|
|
||||||
--- !u!224 &8894486403032071402
|
|
||||||
RectTransform:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 3011982018749398316}
|
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
|
||||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
|
||||||
m_ConstrainProportionsScale: 0
|
|
||||||
m_Children:
|
|
||||||
- {fileID: 3974886479277341081}
|
|
||||||
- {fileID: 5780838202259870402}
|
|
||||||
- {fileID: 1022765863909309765}
|
|
||||||
m_Father: {fileID: 0}
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
|
||||||
m_AnchorMin: {x: 0, y: 0}
|
|
||||||
m_AnchorMax: {x: 1, y: 1}
|
|
||||||
m_AnchoredPosition: {x: 0, y: -76}
|
|
||||||
m_SizeDelta: {x: 0, y: -76}
|
|
||||||
m_Pivot: {x: 0, y: 1}
|
|
||||||
--- !u!114 &9000987459549261387
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 3011982018749398316}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: 7be08eb56899e2042811c8d1da0924a6, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
itemPrefab: {fileID: 4190301731963491743, guid: 3b667b13f22b81b4896e14bd416de1f7, type: 3}
|
|
||||||
root: {fileID: 908297234377844233}
|
|
||||||
allowMultipleSelection: 1
|
|
||||||
enableDragDrop: 1
|
|
||||||
--- !u!222 &4415661564241239138
|
|
||||||
CanvasRenderer:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 3011982018749398316}
|
|
||||||
m_CullTransparentMesh: 1
|
|
||||||
--- !u!114 &2006518343008415563
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 3011982018749398316}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_Material: {fileID: 0}
|
|
||||||
m_Color: {r: 0.14509805, g: 0.14509805, b: 0.14901961, a: 1}
|
|
||||||
m_RaycastTarget: 1
|
|
||||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
|
||||||
m_Maskable: 1
|
|
||||||
m_OnCullStateChanged:
|
|
||||||
m_PersistentCalls:
|
|
||||||
m_Calls: []
|
|
||||||
m_Sprite: {fileID: 0}
|
|
||||||
m_Type: 1
|
|
||||||
m_PreserveAspect: 0
|
|
||||||
m_FillCenter: 1
|
|
||||||
m_FillMethod: 4
|
|
||||||
m_FillAmount: 1
|
|
||||||
m_FillClockwise: 1
|
|
||||||
m_FillOrigin: 0
|
|
||||||
m_UseSpriteMesh: 0
|
|
||||||
m_PixelsPerUnitMultiplier: 1
|
|
||||||
--- !u!114 &5742779717166474465
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 3011982018749398316}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: 1aa08ab6e0800fa44ae55d278d1423e3, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_Content: {fileID: 2261173428971837695}
|
|
||||||
m_Horizontal: 1
|
|
||||||
m_Vertical: 1
|
|
||||||
m_MovementType: 2
|
|
||||||
m_Elasticity: 0.1
|
|
||||||
m_Inertia: 1
|
|
||||||
m_DecelerationRate: 0.135
|
|
||||||
m_ScrollSensitivity: 10
|
|
||||||
m_Viewport: {fileID: 3974886479277341081}
|
|
||||||
m_HorizontalScrollbar: {fileID: 6025781672912143612}
|
|
||||||
m_VerticalScrollbar: {fileID: 4856300785373777908}
|
|
||||||
m_HorizontalScrollbarVisibility: 1
|
|
||||||
m_VerticalScrollbarVisibility: 1
|
|
||||||
m_HorizontalScrollbarSpacing: -3
|
|
||||||
m_VerticalScrollbarSpacing: -3
|
|
||||||
m_OnValueChanged:
|
|
||||||
m_PersistentCalls:
|
|
||||||
m_Calls: []
|
|
||||||
--- !u!1 &3904395414431480648
|
|
||||||
GameObject:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
serializedVersion: 6
|
|
||||||
m_Component:
|
|
||||||
- component: {fileID: 3974886479277341081}
|
|
||||||
- component: {fileID: 2640013902256625935}
|
|
||||||
- component: {fileID: 7267482512719985956}
|
|
||||||
- component: {fileID: 1414930982064568929}
|
|
||||||
m_Layer: 5
|
|
||||||
m_Name: Viewport
|
|
||||||
m_TagString: Untagged
|
|
||||||
m_Icon: {fileID: 0}
|
|
||||||
m_NavMeshLayer: 0
|
|
||||||
m_StaticEditorFlags: 0
|
|
||||||
m_IsActive: 1
|
|
||||||
--- !u!224 &3974886479277341081
|
|
||||||
RectTransform:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 3904395414431480648}
|
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
|
||||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
|
||||||
m_ConstrainProportionsScale: 0
|
|
||||||
m_Children:
|
|
||||||
- {fileID: 2261173428971837695}
|
|
||||||
m_Father: {fileID: 8894486403032071402}
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
|
||||||
m_AnchorMin: {x: 0, y: 0}
|
|
||||||
m_AnchorMax: {x: 1, y: 1}
|
|
||||||
m_AnchoredPosition: {x: 0, y: 0}
|
|
||||||
m_SizeDelta: {x: 0, y: 0}
|
|
||||||
m_Pivot: {x: 0, y: 1}
|
|
||||||
--- !u!114 &2640013902256625935
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 3904395414431480648}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: 31a19414c41e5ae4aae2af33fee712f6, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_ShowMaskGraphic: 0
|
|
||||||
--- !u!222 &7267482512719985956
|
|
||||||
CanvasRenderer:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 3904395414431480648}
|
|
||||||
m_CullTransparentMesh: 0
|
|
||||||
--- !u!114 &1414930982064568929
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 3904395414431480648}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_Material: {fileID: 0}
|
|
||||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
|
||||||
m_RaycastTarget: 1
|
|
||||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
|
||||||
m_Maskable: 1
|
|
||||||
m_OnCullStateChanged:
|
|
||||||
m_PersistentCalls:
|
|
||||||
m_Calls: []
|
|
||||||
m_Sprite: {fileID: 10917, guid: 0000000000000000f000000000000000, type: 0}
|
|
||||||
m_Type: 1
|
|
||||||
m_PreserveAspect: 0
|
|
||||||
m_FillCenter: 1
|
|
||||||
m_FillMethod: 4
|
|
||||||
m_FillAmount: 1
|
|
||||||
m_FillClockwise: 1
|
|
||||||
m_FillOrigin: 0
|
|
||||||
m_UseSpriteMesh: 0
|
|
||||||
m_PixelsPerUnitMultiplier: 1
|
|
||||||
--- !u!1 &4125224351293165923
|
|
||||||
GameObject:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
serializedVersion: 6
|
|
||||||
m_Component:
|
|
||||||
- component: {fileID: 6234462523872176539}
|
|
||||||
- component: {fileID: 68695882802577982}
|
|
||||||
- component: {fileID: 7428146289359687929}
|
|
||||||
m_Layer: 5
|
|
||||||
m_Name: Handle
|
|
||||||
m_TagString: Untagged
|
|
||||||
m_Icon: {fileID: 0}
|
|
||||||
m_NavMeshLayer: 0
|
|
||||||
m_StaticEditorFlags: 0
|
|
||||||
m_IsActive: 1
|
|
||||||
--- !u!224 &6234462523872176539
|
|
||||||
RectTransform:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 4125224351293165923}
|
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
|
||||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
|
||||||
m_ConstrainProportionsScale: 0
|
|
||||||
m_Children: []
|
|
||||||
m_Father: {fileID: 3301928374361144381}
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
|
||||||
m_AnchorMin: {x: 0, y: 0}
|
|
||||||
m_AnchorMax: {x: 0, y: 0}
|
|
||||||
m_AnchoredPosition: {x: 0, y: 0}
|
|
||||||
m_SizeDelta: {x: 20, y: 20}
|
|
||||||
m_Pivot: {x: 0.5, y: 0.5}
|
|
||||||
--- !u!222 &68695882802577982
|
|
||||||
CanvasRenderer:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 4125224351293165923}
|
|
||||||
m_CullTransparentMesh: 0
|
|
||||||
--- !u!114 &7428146289359687929
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 4125224351293165923}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_Material: {fileID: 0}
|
|
||||||
m_Color: {r: 0.2, g: 0.2, b: 0.2, a: 1}
|
|
||||||
m_RaycastTarget: 1
|
|
||||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
|
||||||
m_Maskable: 1
|
|
||||||
m_OnCullStateChanged:
|
|
||||||
m_PersistentCalls:
|
|
||||||
m_Calls: []
|
|
||||||
m_Sprite: {fileID: 0}
|
|
||||||
m_Type: 1
|
|
||||||
m_PreserveAspect: 0
|
|
||||||
m_FillCenter: 1
|
|
||||||
m_FillMethod: 4
|
|
||||||
m_FillAmount: 1
|
|
||||||
m_FillClockwise: 1
|
|
||||||
m_FillOrigin: 0
|
|
||||||
m_UseSpriteMesh: 0
|
|
||||||
m_PixelsPerUnitMultiplier: 1
|
|
||||||
--- !u!1 &4235972654286552744
|
|
||||||
GameObject:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
serializedVersion: 6
|
|
||||||
m_Component:
|
|
||||||
- component: {fileID: 908297234377844233}
|
|
||||||
- component: {fileID: 5053221704473354703}
|
|
||||||
- component: {fileID: 2409632255040810509}
|
|
||||||
- component: {fileID: 3050818479446949986}
|
|
||||||
m_Layer: 5
|
|
||||||
m_Name: Root
|
|
||||||
m_TagString: Untagged
|
|
||||||
m_Icon: {fileID: 0}
|
|
||||||
m_NavMeshLayer: 0
|
|
||||||
m_StaticEditorFlags: 0
|
|
||||||
m_IsActive: 1
|
|
||||||
--- !u!224 &908297234377844233
|
|
||||||
RectTransform:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 4235972654286552744}
|
|
||||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
|
||||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
|
||||||
m_ConstrainProportionsScale: 0
|
|
||||||
m_Children: []
|
|
||||||
m_Father: {fileID: 2261173428971837695}
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
|
||||||
m_AnchorMin: {x: 0, y: 1}
|
|
||||||
m_AnchorMax: {x: 0, y: 1}
|
|
||||||
m_AnchoredPosition: {x: 0, y: 0}
|
|
||||||
m_SizeDelta: {x: 0, y: 0}
|
|
||||||
m_Pivot: {x: 0, y: 1}
|
|
||||||
--- !u!114 &5053221704473354703
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 4235972654286552744}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_Padding:
|
|
||||||
m_Left: 20
|
|
||||||
m_Right: 0
|
|
||||||
m_Top: 2
|
|
||||||
m_Bottom: 0
|
|
||||||
m_ChildAlignment: 0
|
|
||||||
m_Spacing: 0
|
|
||||||
m_ChildForceExpandWidth: 0
|
|
||||||
m_ChildForceExpandHeight: 0
|
|
||||||
m_ChildControlWidth: 0
|
|
||||||
m_ChildControlHeight: 0
|
|
||||||
m_ChildScaleWidth: 0
|
|
||||||
m_ChildScaleHeight: 0
|
|
||||||
m_ReverseArrangement: 0
|
|
||||||
--- !u!114 &2409632255040810509
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 4235972654286552744}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_HorizontalFit: 2
|
|
||||||
m_VerticalFit: 2
|
|
||||||
--- !u!114 &3050818479446949986
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 4235972654286552744}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: de4afb3a0d93f05448f2fc60683275c0, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
EnableWidth: 0
|
|
||||||
EnableHeight: 1
|
|
||||||
margin: {x: 0, y: 0}
|
|
||||||
target:
|
|
||||||
- {fileID: 2261173428971837695}
|
|
||||||
--- !u!1 &4766152670497772902
|
|
||||||
GameObject:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
serializedVersion: 6
|
|
||||||
m_Component:
|
|
||||||
- component: {fileID: 3097322909638401089}
|
|
||||||
m_Layer: 5
|
|
||||||
m_Name: Sliding Area
|
|
||||||
m_TagString: Untagged
|
|
||||||
m_Icon: {fileID: 0}
|
|
||||||
m_NavMeshLayer: 0
|
|
||||||
m_StaticEditorFlags: 0
|
|
||||||
m_IsActive: 1
|
|
||||||
--- !u!224 &3097322909638401089
|
|
||||||
RectTransform:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 4766152670497772902}
|
|
||||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
|
||||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
|
||||||
m_ConstrainProportionsScale: 0
|
|
||||||
m_Children:
|
|
||||||
- {fileID: 6081086258189437538}
|
|
||||||
m_Father: {fileID: 1022765863909309765}
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
|
||||||
m_AnchorMin: {x: 0, y: 0}
|
|
||||||
m_AnchorMax: {x: 1, y: 1}
|
|
||||||
m_AnchoredPosition: {x: 0, y: 0}
|
|
||||||
m_SizeDelta: {x: -20, y: -20}
|
|
||||||
m_Pivot: {x: 0.5, y: 0.5}
|
|
||||||
--- !u!1 &4888954506875524224
|
|
||||||
GameObject:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
serializedVersion: 6
|
|
||||||
m_Component:
|
|
||||||
- component: {fileID: 6081086258189437538}
|
|
||||||
- component: {fileID: 1173046865919000884}
|
|
||||||
- component: {fileID: 2680734087760533677}
|
|
||||||
m_Layer: 5
|
|
||||||
m_Name: Handle
|
|
||||||
m_TagString: Untagged
|
|
||||||
m_Icon: {fileID: 0}
|
|
||||||
m_NavMeshLayer: 0
|
|
||||||
m_StaticEditorFlags: 0
|
|
||||||
m_IsActive: 1
|
|
||||||
--- !u!224 &6081086258189437538
|
|
||||||
RectTransform:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 4888954506875524224}
|
|
||||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
|
||||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
|
||||||
m_ConstrainProportionsScale: 0
|
|
||||||
m_Children: []
|
|
||||||
m_Father: {fileID: 3097322909638401089}
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
|
||||||
m_AnchorMin: {x: 0, y: 0}
|
|
||||||
m_AnchorMax: {x: 0, y: 0}
|
|
||||||
m_AnchoredPosition: {x: 0, y: 0}
|
|
||||||
m_SizeDelta: {x: 20, y: 20}
|
|
||||||
m_Pivot: {x: 0.5, y: 0.5}
|
|
||||||
--- !u!222 &1173046865919000884
|
|
||||||
CanvasRenderer:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 4888954506875524224}
|
|
||||||
m_CullTransparentMesh: 0
|
|
||||||
--- !u!114 &2680734087760533677
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 4888954506875524224}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
m_Material: {fileID: 0}
|
|
||||||
m_Color: {r: 0.2, g: 0.2, b: 0.2, a: 1}
|
|
||||||
m_RaycastTarget: 1
|
|
||||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
|
||||||
m_Maskable: 1
|
|
||||||
m_OnCullStateChanged:
|
|
||||||
m_PersistentCalls:
|
|
||||||
m_Calls: []
|
|
||||||
m_Sprite: {fileID: 0}
|
|
||||||
m_Type: 1
|
|
||||||
m_PreserveAspect: 0
|
|
||||||
m_FillCenter: 1
|
|
||||||
m_FillMethod: 4
|
|
||||||
m_FillAmount: 1
|
|
||||||
m_FillClockwise: 1
|
|
||||||
m_FillOrigin: 0
|
|
||||||
m_UseSpriteMesh: 0
|
|
||||||
m_PixelsPerUnitMultiplier: 1
|
|
||||||
--- !u!1 &8720199997050006827
|
|
||||||
GameObject:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
serializedVersion: 6
|
|
||||||
m_Component:
|
|
||||||
- component: {fileID: 2261173428971837695}
|
|
||||||
m_Layer: 5
|
|
||||||
m_Name: Content
|
|
||||||
m_TagString: Untagged
|
|
||||||
m_Icon: {fileID: 0}
|
|
||||||
m_NavMeshLayer: 0
|
|
||||||
m_StaticEditorFlags: 0
|
|
||||||
m_IsActive: 1
|
|
||||||
--- !u!224 &2261173428971837695
|
|
||||||
RectTransform:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 8720199997050006827}
|
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
|
||||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
|
||||||
m_ConstrainProportionsScale: 0
|
|
||||||
m_Children:
|
|
||||||
- {fileID: 908297234377844233}
|
|
||||||
m_Father: {fileID: 3974886479277341081}
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
|
||||||
m_AnchorMin: {x: 0, y: 1}
|
|
||||||
m_AnchorMax: {x: 1, y: 1}
|
|
||||||
m_AnchoredPosition: {x: 0, y: 0}
|
|
||||||
m_SizeDelta: {x: 0, y: 2}
|
|
||||||
m_Pivot: {x: 0, y: 1}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 6d3ce8d71bb960e49a93e9ac847b300f
|
|
||||||
PrefabImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: a1c7ff75bed3103408e2cbe323126b3f
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using UnityEditor;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.UIElements;
|
|
||||||
|
|
||||||
public class GantChartView : EditorWindow
|
|
||||||
{
|
|
||||||
[SerializeField]
|
|
||||||
private VisualTreeAsset m_VisualTreeAsset = default;
|
|
||||||
|
|
||||||
[MenuItem("Window/UI Toolkit/GantChartView")]
|
|
||||||
public static void ShowExample()
|
|
||||||
{
|
|
||||||
GantChartView wnd = GetWindow<GantChartView>();
|
|
||||||
wnd.titleContent = new GUIContent("GantChartView");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CreateGUI()
|
|
||||||
{
|
|
||||||
// Each editor window contains a root VisualElement object
|
|
||||||
VisualElement root = rootVisualElement;
|
|
||||||
|
|
||||||
// VisualElements objects can contain other VisualElement following a tree hierarchy.
|
|
||||||
VisualElement label = new Label("Hello World! From C#");
|
|
||||||
root.Add(label);
|
|
||||||
|
|
||||||
// Instantiate UXML
|
|
||||||
VisualElement labelFromUXML = m_VisualTreeAsset.Instantiate();
|
|
||||||
root.Add(labelFromUXML);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: d7cfb84525061514b899380405a7d881
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences:
|
|
||||||
- m_VisualTreeAsset: {fileID: 9197481963319205126, guid: c4ef95400fcdb23448d991f05d022741, type: 3}
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
.custom-label {
|
|
||||||
font-size: 20px;
|
|
||||||
-unity-font-style: bold;
|
|
||||||
color: rgb(68, 138, 255);
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: acd6a9051b5206b4db77f775332e354b
|
|
||||||
ScriptedImporter:
|
|
||||||
internalIDToNameTable: []
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
disableValidation: 0
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<engine:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:engine="UnityEngine.UIElements" xmlns:editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
|
|
||||||
<Style src="project://database/Assets/Resources/SHI/UIToolkit/GantChartView.uss?fileID=7433441132597879392&guid=acd6a9051b5206b4db77f775332e354b&type=3#GantChartView" />
|
|
||||||
<engine:Label text="Hello World! From UXML" />
|
|
||||||
<engine:Label text="Hello World! With Style" class="custom-label" />
|
|
||||||
</engine:UXML>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: c4ef95400fcdb23448d991f05d022741
|
|
||||||
ScriptedImporter:
|
|
||||||
internalIDToNameTable: []
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 69d1bf66433281745b84b52616b8b8c7
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
#task-list-header {
|
|
||||||
align-items: flex-start;
|
|
||||||
padding-top: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#gantt-container {
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-direction: row;
|
|
||||||
background-color: rgb(250, 250, 250);
|
|
||||||
}
|
|
||||||
|
|
||||||
#task-list-panel {
|
|
||||||
width: 390px;
|
|
||||||
background-color: white;
|
|
||||||
border-right-width: 1px;
|
|
||||||
border-right-color: rgb(51, 51, 51);
|
|
||||||
}
|
|
||||||
|
|
||||||
#task-list-header {
|
|
||||||
height: 54px;
|
|
||||||
background-color: rgb(123, 183, 226);
|
|
||||||
border-bottom-width: 1px;
|
|
||||||
border-bottom-color: black;
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 0;
|
|
||||||
font-size: 12px;
|
|
||||||
-unity-text-align: upper-center;
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#task-list-header Label {
|
|
||||||
font-size: 12px;
|
|
||||||
-unity-font-style: normal;
|
|
||||||
color: black;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
-unity-background-scale-mode: stretch-to-fill;
|
|
||||||
justify-content: center;
|
|
||||||
align-self: center;
|
|
||||||
align-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
-unity-text-align: middle-left;
|
|
||||||
}
|
|
||||||
|
|
||||||
#task-list-view {
|
|
||||||
flex-grow: 1;
|
|
||||||
background-color: rgb(228, 236, 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
#task-list-view .unity-list-view__item {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-row {
|
|
||||||
flex-direction: row;
|
|
||||||
height: 49px;
|
|
||||||
width: 390px;
|
|
||||||
background-color: rgb(136, 190, 230);
|
|
||||||
border-top-style: solid;
|
|
||||||
border-bottom-style: solid;
|
|
||||||
border-left-style: solid;
|
|
||||||
border-right-style: solid;
|
|
||||||
border-top-width: 0;
|
|
||||||
border-bottom-width: 0;
|
|
||||||
border-left-width: 0;
|
|
||||||
border-right-width: 0;
|
|
||||||
border-bottom-color: rgb(136, 190, 230);
|
|
||||||
border-left-color: rgb(136, 190, 230);
|
|
||||||
border-right-color: rgb(136, 190, 230);
|
|
||||||
box-sizing: border-box;
|
|
||||||
-unity-font-definition: resource('Fonts/Pretendard/Pretendard-Regular');
|
|
||||||
font-size: 10px;
|
|
||||||
color: rgb(34, 34, 34);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-row:first-child {
|
|
||||||
border-top-width: 1px;
|
|
||||||
border-top-color: rgb(136, 190, 230);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-cell {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
-unity-text-align: middle-center;
|
|
||||||
font-size: 10px;
|
|
||||||
color: rgb(34, 34, 34);
|
|
||||||
background-color: rgb(228, 236, 255);
|
|
||||||
border-left-width: 0;
|
|
||||||
border-left-color: rgb(136, 190, 230);
|
|
||||||
border-left-style: solid;
|
|
||||||
box-sizing: border-box;
|
|
||||||
-unity-font-definition: resource('Fonts/Pretendard/Pretendard-Regular');
|
|
||||||
border-top-width: 0;
|
|
||||||
border-right-width: 0;
|
|
||||||
border-bottom-width: 0;
|
|
||||||
margin-top: 0;
|
|
||||||
margin-right: 1px;
|
|
||||||
margin-bottom: 1px;
|
|
||||||
margin-left: 0;
|
|
||||||
padding-top: 0;
|
|
||||||
padding-right: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-cell:first-child {
|
|
||||||
border-left-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-row:hover {
|
|
||||||
background-color: rgb(228, 236, 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
#timeline-panel {
|
|
||||||
flex-grow: 1;
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
#timeline-header {
|
|
||||||
height: 54px;
|
|
||||||
background-color: rgb(240, 240, 240);
|
|
||||||
border-bottom-width: 1px;
|
|
||||||
border-bottom-color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
#time-axis-scroll {
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#time-axis-container {
|
|
||||||
height: 100%;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#timeline-scroll-view {
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 1f4fe30776858ce4e96b520e8f407bc8
|
|
||||||
ScriptedImporter:
|
|
||||||
internalIDToNameTable: []
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
disableValidation: 0
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<ui:UXML xmlns:ui="UnityEngine.UIElements" editor-extension-mode="False">
|
|
||||||
<Style src="project://database/Assets/Resources/SHI/UIToolkit/ISOP/ShipblockGantt.uss?fileID=7433441132597879392&guid=1f4fe30776858ce4e96b520e8f407bc8&type=3#ShipblockGantt" />
|
|
||||||
<ui:VisualElement name="gantt-container">
|
|
||||||
<ui:VisualElement name="task-list-panel">
|
|
||||||
<ui:VisualElement name="task-list-header">
|
|
||||||
<ui:Label text="BLOCK STRUCTURE" />
|
|
||||||
</ui:VisualElement>
|
|
||||||
<ui:ListView name="task-list-view" />
|
|
||||||
</ui:VisualElement>
|
|
||||||
<ui:VisualElement name="timeline-panel">
|
|
||||||
<ui:VisualElement name="timeline-header">
|
|
||||||
<ui:ScrollView name="time-axis-scroll">
|
|
||||||
<ui:VisualElement name="time-axis-container" />
|
|
||||||
</ui:ScrollView>
|
|
||||||
</ui:VisualElement>
|
|
||||||
<ui:ScrollView name="timeline-scroll-view" />
|
|
||||||
</ui:VisualElement>
|
|
||||||
</ui:VisualElement>
|
|
||||||
</ui:UXML>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 2233c37def8330b4381da9ca3928d163
|
|
||||||
ScriptedImporter:
|
|
||||||
internalIDToNameTable: []
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
@@ -1,297 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using TMPro;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.TextCore.Text;
|
|
||||||
using UnityEngine.UIElements;
|
|
||||||
using UVC.Util;
|
|
||||||
|
|
||||||
public class ShipblockGanttController : MonoBehaviour
|
|
||||||
{
|
|
||||||
[SerializeField] private VisualTreeAsset taskRowTemplate; // row with 8 cells
|
|
||||||
[SerializeField] private StyleSheet styleSheet;
|
|
||||||
[SerializeField] private Color planColor = new Color(0.2f, 0.4f, 0.8f, 0.8f);
|
|
||||||
[SerializeField] private Color actualColor = new Color(0.2f, 0.9f, 0.3f, 0.9f);
|
|
||||||
private float pixelsPerDay = 18f; // day cell width
|
|
||||||
[SerializeField] private Font PretendardRegular;
|
|
||||||
|
|
||||||
private VisualElement root;
|
|
||||||
private ListView taskListView;
|
|
||||||
private ScrollView timelineScrollView; // main scroll view (vertical + horizontal)
|
|
||||||
private ScrollView axisScrollView; // header axis scroll view (hidden scrollers, synced)
|
|
||||||
private VisualElement timelineContent;
|
|
||||||
private List<ShipblockTask> tasks;
|
|
||||||
private DateTime projectStartDate;
|
|
||||||
private DateTime projectEndDate;
|
|
||||||
private int totalDays;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public void Load(string jsonFileName)
|
|
||||||
{
|
|
||||||
root = GetComponent<UIDocument>().rootVisualElement;
|
|
||||||
if (styleSheet != null && !root.styleSheets.Contains(styleSheet)) root.styleSheets.Add(styleSheet);
|
|
||||||
|
|
||||||
LoadData(jsonFileName);
|
|
||||||
CalculateProjectRange();
|
|
||||||
InitializeUI();
|
|
||||||
SetupTimeAxis();
|
|
||||||
RenderTasks();
|
|
||||||
}
|
|
||||||
|
|
||||||
void LoadData(string jsonFileName)
|
|
||||||
{
|
|
||||||
var json = File.ReadAllText(jsonFileName);
|
|
||||||
var wrapper = JsonUtility.FromJson<ShipblockDataWrapper>("{\"items\":" + json + "}");
|
|
||||||
tasks = wrapper.items.Where(t => !string.IsNullOrEmpty(t.STDT21) && t.STDT21 != "null").ToList();
|
|
||||||
foreach (var task in tasks)
|
|
||||||
task.CalculatedProgress = CalculateProgress(task);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void CalculateProjectRange()
|
|
||||||
{
|
|
||||||
var dates = new List<DateTime>();
|
|
||||||
foreach (var task in tasks)
|
|
||||||
{
|
|
||||||
if (task.GetPlanStart().HasValue) dates.Add(task.GetPlanStart().Value);
|
|
||||||
if (task.GetPlanEnd().HasValue) dates.Add(task.GetPlanEnd().Value);
|
|
||||||
if (task.GetActualStart().HasValue) dates.Add(task.GetActualStart().Value);
|
|
||||||
if (task.GetActualEnd().HasValue) dates.Add(task.GetActualEnd().Value);
|
|
||||||
}
|
|
||||||
projectStartDate = dates.Any() ? dates.Min().AddDays(-3) : new DateTime(2025, 1, 6);
|
|
||||||
projectEndDate = dates.Any() ? dates.Max().AddDays(3) : new DateTime(2025, 2, 28);
|
|
||||||
totalDays = (int)(projectEndDate - projectStartDate).TotalDays + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void InitializeUI()
|
|
||||||
{
|
|
||||||
taskListView = root.Q<ListView>("task-list-view");
|
|
||||||
taskListView.itemsSource = tasks;
|
|
||||||
taskListView.makeItem = () => taskRowTemplate.CloneTree();
|
|
||||||
taskListView.bindItem = (ve, i) => BindTaskRow(ve, tasks[i]);
|
|
||||||
taskListView.fixedItemHeight = 49;
|
|
||||||
taskListView.selectionType = SelectionType.None;
|
|
||||||
taskListView.showAlternatingRowBackgrounds = AlternatingRowBackground.None;
|
|
||||||
taskListView.reorderable = false;
|
|
||||||
taskListView.showBoundCollectionSize = false;
|
|
||||||
taskListView.virtualizationMethod = CollectionVirtualizationMethod.FixedHeight;
|
|
||||||
var internalScroll = taskListView.Q<ScrollView>();
|
|
||||||
if (internalScroll != null)
|
|
||||||
{
|
|
||||||
internalScroll.verticalScrollerVisibility = ScrollerVisibility.Hidden;
|
|
||||||
internalScroll.horizontalScrollerVisibility = ScrollerVisibility.Hidden;
|
|
||||||
}
|
|
||||||
axisScrollView = root.Q<ScrollView>("time-axis-scroll");
|
|
||||||
if (axisScrollView != null)
|
|
||||||
{
|
|
||||||
axisScrollView.verticalScrollerVisibility = ScrollerVisibility.Hidden;
|
|
||||||
axisScrollView.horizontalScrollerVisibility = ScrollerVisibility.Hidden;
|
|
||||||
}
|
|
||||||
timelineScrollView = root.Q<ScrollView>("timeline-scroll-view");
|
|
||||||
if (timelineScrollView != null)
|
|
||||||
{
|
|
||||||
timelineScrollView.mode = ScrollViewMode.VerticalAndHorizontal;
|
|
||||||
timelineContent = timelineScrollView.Q<VisualElement>("unity-content-container");
|
|
||||||
if (timelineContent != null)
|
|
||||||
timelineContent.style.width = totalDays * pixelsPerDay;
|
|
||||||
}
|
|
||||||
if (internalScroll != null && timelineScrollView != null)
|
|
||||||
{
|
|
||||||
internalScroll.verticalScroller.valueChanged += v => timelineScrollView.verticalScroller.value = v;
|
|
||||||
timelineScrollView.verticalScroller.valueChanged += v => internalScroll.verticalScroller.value = v;
|
|
||||||
}
|
|
||||||
if (axisScrollView != null && timelineScrollView != null)
|
|
||||||
{
|
|
||||||
axisScrollView.horizontalScroller.valueChanged += v => timelineScrollView.horizontalScroller.value = v;
|
|
||||||
timelineScrollView.horizontalScroller.valueChanged += v => axisScrollView.horizontalScroller.value = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void BindTaskRow(VisualElement row, ShipblockTask task)
|
|
||||||
{
|
|
||||||
Set(row, "c1", task?.L1); Set(row, "c2", task?.L2); Set(row, "c3", task?.L3); Set(row, "c4", task?.L4);
|
|
||||||
Set(row, "c5", task?.L5); Set(row, "c6", task?.L6); Set(row, "c7", task?.L7); Set(row, "c8", task?.L8);
|
|
||||||
row.tooltip = task != null ? $"{task.BLK_NO} | {task.SHIP_TYPE}" : string.Empty;
|
|
||||||
}
|
|
||||||
static void Set(VisualElement row, string name, string text)
|
|
||||||
{ var lab = row.Q<UnityEngine.UIElements.Label>(name); if (lab != null) lab.text = string.IsNullOrEmpty(text) ? string.Empty : text; }
|
|
||||||
|
|
||||||
void SetupTimeAxis() => SetupThreeTierHeader();
|
|
||||||
|
|
||||||
void SetupThreeTierHeader()
|
|
||||||
{
|
|
||||||
var container = root.Q<VisualElement>("time-axis-container");
|
|
||||||
container.Clear();
|
|
||||||
container.style.height = pixelsPerDay * 3; // total header height
|
|
||||||
|
|
||||||
// Background base grid style color
|
|
||||||
Color lineColor = ColorUtil.FromHex("#7BB7E2"); // light blue lines
|
|
||||||
|
|
||||||
var monthsLayer = new VisualElement { name = "months-layer" };
|
|
||||||
var weeksLayer = new VisualElement { name = "weeks-layer" };
|
|
||||||
var daysLayer = new VisualElement { name = "days-layer" };
|
|
||||||
foreach (var layer in new[] { monthsLayer, weeksLayer, daysLayer })
|
|
||||||
{
|
|
||||||
layer.style.position = Position.Absolute;
|
|
||||||
layer.style.left = 0;
|
|
||||||
layer.style.top = 0;
|
|
||||||
layer.style.height = pixelsPerDay * 3;
|
|
||||||
layer.style.width = totalDays * pixelsPerDay;
|
|
||||||
}
|
|
||||||
container.Add(monthsLayer); container.Add(weeksLayer); container.Add(daysLayer);
|
|
||||||
|
|
||||||
DateTime cursor = projectStartDate;
|
|
||||||
while (cursor <= projectEndDate)
|
|
||||||
{
|
|
||||||
DateTime monthStart = new DateTime(cursor.Year, cursor.Month, 1);
|
|
||||||
DateTime nextMonthStart = monthStart.AddMonths(1);
|
|
||||||
DateTime segmentStart = cursor; // first day we actually display within month
|
|
||||||
DateTime segmentEnd = nextMonthStart.AddDays(-1) < projectEndDate ? nextMonthStart.AddDays(-1) : projectEndDate;
|
|
||||||
int startIndex = (int)(segmentStart - projectStartDate).TotalDays;
|
|
||||||
int spanDays = (int)(segmentEnd - segmentStart).TotalDays + 1;
|
|
||||||
AddMonthLabel(monthsLayer, segmentStart.Year, segmentStart.Month, startIndex, spanDays, lineColor);
|
|
||||||
|
|
||||||
// Weeks inside month
|
|
||||||
DateTime weekCursor = segmentStart;
|
|
||||||
int weekOfMonth = 1; // label weeks sequentially per month
|
|
||||||
while (weekCursor <= segmentEnd)
|
|
||||||
{
|
|
||||||
DateTime weekStart = weekCursor;
|
|
||||||
// Keep the existing segmentation logic (end at Sunday or month end)
|
|
||||||
DateTime tentativeEnd = weekStart.AddDays(7 - (int)weekStart.DayOfWeek); // until Sunday
|
|
||||||
DateTime weekEnd = tentativeEnd > segmentEnd ? segmentEnd : tentativeEnd;
|
|
||||||
int weekStartIndex = (int)(weekStart - projectStartDate).TotalDays;
|
|
||||||
int weekSpanDays = (int)(weekEnd - weekStart).TotalDays + 1;
|
|
||||||
AddWeekLabel(weeksLayer, weekOfMonth, weekStartIndex, weekSpanDays, lineColor);
|
|
||||||
weekCursor = weekEnd.AddDays(1);
|
|
||||||
weekOfMonth++;
|
|
||||||
}
|
|
||||||
cursor = segmentEnd.AddDays(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Days line
|
|
||||||
for (int i = 0; i < totalDays; i++)
|
|
||||||
{
|
|
||||||
DateTime date = projectStartDate.AddDays(i);
|
|
||||||
AddDayCell(daysLayer, date.Day, i, lineColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void AddMonthLabel(VisualElement parent, int year, int month, int startIndex, int spanDays, Color lineColor)
|
|
||||||
{
|
|
||||||
var ve = new VisualElement();
|
|
||||||
ve.style.position = Position.Absolute; ve.style.left = startIndex * pixelsPerDay; ve.style.top = 0;
|
|
||||||
ve.style.width = spanDays * pixelsPerDay; ve.style.height = pixelsPerDay;
|
|
||||||
ve.style.borderLeftColor = lineColor; ve.style.borderLeftWidth = startIndex == 0 ? 0 : 1;
|
|
||||||
ve.style.borderBottomColor = lineColor; ve.style.borderBottomWidth = 1;
|
|
||||||
var lab = new Label($"{year}년 {month}월"); lab.style.unityTextAlign = TextAnchor.MiddleCenter; lab.style.unityFontStyleAndWeight = FontStyle.Normal; lab.style.color = Color.black;
|
|
||||||
lab.style.position = Position.Absolute; lab.style.left = 0; lab.style.top = 0; lab.style.width = spanDays * pixelsPerDay; lab.style.height = pixelsPerDay;
|
|
||||||
lab.style.unityFontDefinition = FontDefinition.FromFont(PretendardRegular);
|
|
||||||
lab.style.fontSize = 10;
|
|
||||||
lab.style.marginBottom = 0;
|
|
||||||
lab.style.marginLeft = 0;
|
|
||||||
lab.style.marginRight = 0;
|
|
||||||
lab.style.marginTop = 0;
|
|
||||||
lab.style.paddingBottom = 0;
|
|
||||||
lab.style.paddingLeft = 0;
|
|
||||||
lab.style.paddingRight = 0;
|
|
||||||
lab.style.paddingTop = 0;
|
|
||||||
ve.Add(lab);
|
|
||||||
parent.Add(ve);
|
|
||||||
}
|
|
||||||
|
|
||||||
void AddWeekLabel(VisualElement parent, int weekOfMonth, int startIndex, int spanDays, Color lineColor)
|
|
||||||
{
|
|
||||||
var ve = new VisualElement();
|
|
||||||
ve.style.position = Position.Absolute; ve.style.left = startIndex * pixelsPerDay; ve.style.top = pixelsPerDay;
|
|
||||||
ve.style.width = spanDays * pixelsPerDay; ve.style.height = pixelsPerDay;
|
|
||||||
ve.style.borderLeftColor = lineColor; ve.style.borderLeftWidth = startIndex == 0 ? 0 : 1;
|
|
||||||
ve.style.borderBottomColor = lineColor; ve.style.borderBottomWidth = 1;
|
|
||||||
var lab = new Label($"{weekOfMonth}주"); lab.style.unityTextAlign = TextAnchor.MiddleCenter; lab.style.color = Color.black;
|
|
||||||
lab.style.position = Position.Absolute; lab.style.left = 0; lab.style.top = 0; lab.style.width = spanDays * pixelsPerDay; lab.style.height = pixelsPerDay;
|
|
||||||
lab.style.unityFontDefinition = FontDefinition.FromFont(PretendardRegular);
|
|
||||||
lab.style.fontSize = 10;
|
|
||||||
lab.style.marginBottom = 0;
|
|
||||||
lab.style.marginLeft = 0;
|
|
||||||
lab.style.marginRight = 0;
|
|
||||||
lab.style.marginTop = 0;
|
|
||||||
lab.style.paddingBottom = 0;
|
|
||||||
lab.style.paddingLeft = 0;
|
|
||||||
lab.style.paddingRight = 0;
|
|
||||||
lab.style.paddingTop = 0;
|
|
||||||
ve.Add(lab);
|
|
||||||
parent.Add(ve);
|
|
||||||
}
|
|
||||||
|
|
||||||
void AddDayCell(VisualElement parent, int day, int dayIndex, Color lineColor)
|
|
||||||
{
|
|
||||||
var ve = new VisualElement();
|
|
||||||
ve.style.position = Position.Absolute; ve.style.left = dayIndex * pixelsPerDay; ve.style.top = pixelsPerDay * 2;
|
|
||||||
ve.style.width = pixelsPerDay; ve.style.height = pixelsPerDay;
|
|
||||||
ve.style.borderLeftColor = lineColor; ve.style.borderLeftWidth = dayIndex == 0 ? 0 : 1;
|
|
||||||
var lab = new Label(day.ToString("00")); lab.style.unityTextAlign = TextAnchor.MiddleCenter; lab.style.fontSize = 10; lab.style.color = Color.black;
|
|
||||||
lab.style.position = Position.Absolute; lab.style.left = 0; lab.style.top = 0; lab.style.width = pixelsPerDay; lab.style.height = pixelsPerDay;
|
|
||||||
lab.style.unityFontDefinition = FontDefinition.FromFont(PretendardRegular);
|
|
||||||
lab.style.fontSize = 10;
|
|
||||||
lab.style.marginBottom = 0;
|
|
||||||
lab.style.marginLeft = 0;
|
|
||||||
lab.style.marginRight = 0;
|
|
||||||
lab.style.marginTop = 0;
|
|
||||||
lab.style.paddingBottom = 0;
|
|
||||||
lab.style.paddingLeft = 0;
|
|
||||||
lab.style.paddingRight = 0;
|
|
||||||
lab.style.paddingTop = 0;
|
|
||||||
ve.Add(lab);
|
|
||||||
parent.Add(ve);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RenderTasks()
|
|
||||||
{
|
|
||||||
if (timelineContent == null) return;
|
|
||||||
timelineContent.Clear();
|
|
||||||
for (int i = 0; i < tasks.Count; i++)
|
|
||||||
{
|
|
||||||
var task = tasks[i];
|
|
||||||
var row = new VisualElement();
|
|
||||||
row.style.height = 40; row.style.flexDirection = FlexDirection.Row; row.style.position = Position.Relative;
|
|
||||||
row.style.borderBottomWidth = 1; row.style.borderBottomColor = new Color(0.9f, 0.9f, 0.9f);
|
|
||||||
if (task.GetPlanStart().HasValue && task.GetPlanEnd().HasValue)
|
|
||||||
row.Add(CreateBar(task, task.GetPlanStart().Value, task.GetPlanEnd().Value, planColor, 10));
|
|
||||||
if (task.GetActualStart().HasValue && task.GetActualEnd().HasValue)
|
|
||||||
row.Add(CreateBar(task, task.GetActualStart().Value, task.GetActualEnd().Value, actualColor, 22));
|
|
||||||
var progressLabel = new Label($"{Mathf.RoundToInt(task.CalculatedProgress)}%");
|
|
||||||
progressLabel.style.position = Position.Absolute; progressLabel.style.right = 8; progressLabel.style.top = 10; progressLabel.style.fontSize = 10; progressLabel.style.color = Color.black; progressLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
|
||||||
row.Add(progressLabel);
|
|
||||||
timelineContent.Add(row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VisualElement CreateBar(ShipblockTask task, DateTime start, DateTime end, Color color, int top)
|
|
||||||
{
|
|
||||||
var bar = new VisualElement();
|
|
||||||
bar.style.position = Position.Absolute; bar.style.backgroundColor = color; bar.style.height = 12; bar.style.top = top;
|
|
||||||
bar.style.borderBottomLeftRadius = 2; bar.style.borderBottomRightRadius = 2; bar.style.borderTopLeftRadius = 2; bar.style.borderTopRightRadius = 2;
|
|
||||||
float startX = (float)(start - projectStartDate).TotalDays * pixelsPerDay;
|
|
||||||
float width = Mathf.Max((float)(end - start).TotalDays * pixelsPerDay, 6);
|
|
||||||
bar.style.left = startX; bar.style.width = width;
|
|
||||||
bar.tooltip = $"{task.GetDisplayName()}\n{start:MM/dd} ~ {end:MM/dd}";
|
|
||||||
return bar;
|
|
||||||
}
|
|
||||||
|
|
||||||
float CalculateProgress(ShipblockTask task)
|
|
||||||
{
|
|
||||||
var planStart = task.GetPlanStart(); var planEnd = task.GetPlanEnd(); var actualEnd = task.GetActualEnd();
|
|
||||||
if (!planStart.HasValue || !planEnd.HasValue) return 0;
|
|
||||||
float total = (float)(planEnd.Value - planStart.Value).TotalDays; if (total <= 0) return 0;
|
|
||||||
if (actualEnd.HasValue)
|
|
||||||
{ float actualDays = (float)(actualEnd.Value - planStart.Value).TotalDays; return Mathf.Clamp(actualDays / total * 100, 0, 100); }
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public class JSONWrapper { public List<ShipblockTask> items; }
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 45d3090dd89645248a052afa1122996f
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public class ShipblockTask
|
|
||||||
{
|
|
||||||
// 기본 정보
|
|
||||||
public string PROJ_NO;
|
|
||||||
public string BLK_NO;
|
|
||||||
public string L1;
|
|
||||||
public string L2;
|
|
||||||
public string L3;
|
|
||||||
public string L4;
|
|
||||||
public string L5;
|
|
||||||
public string L6;
|
|
||||||
public string L7;
|
|
||||||
public string L8;
|
|
||||||
public string SHIP_TYPE;
|
|
||||||
|
|
||||||
// 계획 일정 (파란색 막대)
|
|
||||||
public string STDT21;
|
|
||||||
public string FNDT21;
|
|
||||||
public int DUR21;
|
|
||||||
|
|
||||||
// 실적 일정 (연두색 막대)
|
|
||||||
public string STDT23;
|
|
||||||
public string FNDT23;
|
|
||||||
public int DUR23;
|
|
||||||
|
|
||||||
// 추가 작업 코드들...
|
|
||||||
public string STDT43, FNDT43; public int DUR43;
|
|
||||||
public string STDT44, FNDT44; public int DUR44;
|
|
||||||
public string STDT46, FNDT46; public int DUR46;
|
|
||||||
public string STDT49, FNDT49; public int DUR49;
|
|
||||||
public string STDT4A, FNDT4A; public int DUR4A;
|
|
||||||
public string STDT4B, FNDT4B; public int DUR4B;
|
|
||||||
public string STDT62, FNDT62; public int DUR62;
|
|
||||||
|
|
||||||
[NonSerialized] public float CalculatedProgress;
|
|
||||||
|
|
||||||
public DateTime? GetPlanStart() => ParseDate(STDT21);
|
|
||||||
public DateTime? GetPlanEnd() => ParseDate(FNDT21);
|
|
||||||
public DateTime? GetActualStart() => ParseDate(STDT23);
|
|
||||||
public DateTime? GetActualEnd() => ParseDate(FNDT23);
|
|
||||||
|
|
||||||
private DateTime? ParseDate(string dateStr)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(dateStr) || dateStr == "null") return null;
|
|
||||||
if (DateTime.TryParseExact(dateStr, "yyyyMMdd", null, System.Globalization.DateTimeStyles.None, out DateTime date))
|
|
||||||
return date;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetDisplayName()
|
|
||||||
{
|
|
||||||
// L1~L8 중 가장 하위 유효값
|
|
||||||
if (!string.IsNullOrEmpty(L8)) return L8;
|
|
||||||
if (!string.IsNullOrEmpty(L7)) return L7;
|
|
||||||
if (!string.IsNullOrEmpty(L6)) return L6;
|
|
||||||
if (!string.IsNullOrEmpty(L5)) return L5;
|
|
||||||
if (!string.IsNullOrEmpty(L4)) return L4;
|
|
||||||
if (!string.IsNullOrEmpty(L3)) return L3;
|
|
||||||
if (!string.IsNullOrEmpty(L2)) return L2;
|
|
||||||
return L1 ?? BLK_NO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public class ShipblockDataWrapper
|
|
||||||
{
|
|
||||||
public List<ShipblockTask> items;
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 17fd71296384e3045873a6553d88a065
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<ui:UXML xmlns:ui="UnityEngine.UIElements" editor-extension-mode="False">
|
|
||||||
<Style src="project://database/Assets/Resources/SHI/UIToolkit/ISOP/ShipblockGantt.uss?fileID=7433441132597879392&guid=1f4fe30776858ce4e96b520e8f407bc8&type=3#ShipblockGantt" />
|
|
||||||
<ui:VisualElement name="task-row" class="task-row">
|
|
||||||
<ui:Label name="c1" class="task-cell" />
|
|
||||||
<ui:Label name="c2" class="task-cell" />
|
|
||||||
<ui:Label name="c3" class="task-cell" />
|
|
||||||
<ui:Label name="c4" class="task-cell" />
|
|
||||||
<ui:Label name="c5" class="task-cell" />
|
|
||||||
<ui:Label name="c6" class="task-cell" />
|
|
||||||
<ui:Label name="c7" class="task-cell" />
|
|
||||||
<ui:Label name="c8" class="task-cell" />
|
|
||||||
</ui:VisualElement>
|
|
||||||
</ui:UXML>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: c6a7ece3f9f2c8449acb7b00aa7a7038
|
|
||||||
ScriptedImporter:
|
|
||||||
internalIDToNameTable: []
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
%YAML 1.1
|
|
||||||
%TAG !u! tag:unity3d.com,2011:
|
|
||||||
--- !u!114 &11400000
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 19101, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
m_Name: PanelSettings
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
themeUss: {fileID: -4733365628477956816, guid: 7f6362c1cd0b3074ab0f0f88dd27536b, type: 3}
|
|
||||||
m_DisableNoThemeWarning: 0
|
|
||||||
m_TargetTexture: {fileID: 0}
|
|
||||||
m_RenderMode: 0
|
|
||||||
m_WorldSpaceLayer: 0
|
|
||||||
m_ScaleMode: 1
|
|
||||||
m_ReferenceSpritePixelsPerUnit: 100
|
|
||||||
m_PixelsPerUnit: 100
|
|
||||||
m_Scale: 1
|
|
||||||
m_ReferenceDpi: 96
|
|
||||||
m_FallbackDpi: 96
|
|
||||||
m_ReferenceResolution: {x: 1200, y: 800}
|
|
||||||
m_ScreenMatchMode: 0
|
|
||||||
m_Match: 0
|
|
||||||
m_SortingOrder: 0
|
|
||||||
m_TargetDisplay: 0
|
|
||||||
m_BindingLogLevel: 0
|
|
||||||
m_ClearDepthStencil: 1
|
|
||||||
m_ClearColor: 0
|
|
||||||
m_ColorClearValue: {r: 0, g: 0, b: 0, a: 0}
|
|
||||||
m_VertexBudget: 0
|
|
||||||
m_DynamicAtlasSettings:
|
|
||||||
m_MinAtlasSize: 64
|
|
||||||
m_MaxAtlasSize: 4096
|
|
||||||
m_MaxSubTextureSize: 64
|
|
||||||
m_ActiveFilters: -1
|
|
||||||
m_AtlasBlitShader: {fileID: 9101, guid: 0000000000000000f000000000000000, type: 0}
|
|
||||||
m_RuntimeShader: {fileID: 9100, guid: 0000000000000000f000000000000000, type: 0}
|
|
||||||
m_RuntimeWorldShader: {fileID: 9102, guid: 0000000000000000f000000000000000, type: 0}
|
|
||||||
m_ICUDataAsset: {fileID: 0}
|
|
||||||
forceGammaRendering: 0
|
|
||||||
textSettings: {fileID: 0}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 8b377a015f4b6e3488cbc9c475f34132
|
|
||||||
NativeFormatImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
mainObjectFileID: 11400000
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 64fb2d86b4390f04287dd53425bd2316
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
/* Basic USS for Gantt */
|
|
||||||
.header-container { }
|
|
||||||
.month-row { }
|
|
||||||
.week-row { }
|
|
||||||
.day-row { }
|
|
||||||
.rows-container { }
|
|
||||||
.chart-row { flex-direction: row; height:28px; position: relative; border-bottom:1px solid #3A3A3A; }
|
|
||||||
.hierarchy-cell { font-size:11px; width:240px; padding-left:4px; }
|
|
||||||
.segments-layer { flex-grow:1; position: relative; }
|
|
||||||
.segment { border-radius:3px; height:22px; background-color: rgba(64,128,220,0.6); }
|
|
||||||
.marker { color:#2AA3FF; font-size:12px; }
|
|
||||||
.row-selected { background-color: rgba(255,255,0,0.15); }
|
|
||||||
.segment-selected { outline:2px solid #FFD500; }
|
|
||||||
.span-cell { text-align:center; font-size:11px; border-right:1px solid #222; }
|
|
||||||
.day-cell { text-align:center; font-size:10px; border-right:1px solid #333; width:16px; }
|
|
||||||
.today-line { position:absolute; width:1px; background:#ff6b00; top:0; bottom:0; }
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 4557a013dc4822f499286c5a202a41a5
|
|
||||||
ScriptedImporter:
|
|
||||||
internalIDToNameTable: []
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
|
|
||||||
disableValidation: 0
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using Cysharp.Threading.Tasks;
|
using Cysharp.Threading.Tasks;
|
||||||
using SHI.modal;
|
|
||||||
using SHI.Modal.ISOP;
|
using SHI.Modal.ISOP;
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,407 +0,0 @@
|
|||||||
using Best.HTTP.JSON.LitJson;
|
|
||||||
using Cysharp.Threading.Tasks;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.UI;
|
|
||||||
using UVC.Json;
|
|
||||||
|
|
||||||
namespace SHI.modal
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 모달 패널 내부에서 모델 뷰, 계층 리스트, 간트 차트를 조율하는 컨트롤러입니다.
|
|
||||||
/// 컴포넌트 간 선택 동기화, 레이아웃 분할 제어, 데이터 로딩을 담당합니다.
|
|
||||||
/// </summary>
|
|
||||||
public class BlockDetailModal : MonoBehaviour
|
|
||||||
{
|
|
||||||
[Header("References")]
|
|
||||||
[SerializeField] private Button closeButton;
|
|
||||||
[SerializeField] private ModelDetailListView listView;
|
|
||||||
[SerializeField] private ModelDetailView modelView;
|
|
||||||
[SerializeField] private ModelDetailISOPChartView chartView;
|
|
||||||
|
|
||||||
[Header("UI Controls")]
|
|
||||||
[SerializeField] private Button modelViewExpandButton;
|
|
||||||
[SerializeField] private Button chartViewExpandButton;
|
|
||||||
[SerializeField] private Button dragButton;
|
|
||||||
[SerializeField] private Button showListButton;
|
|
||||||
|
|
||||||
// split 제어용 캐시
|
|
||||||
private LayoutElement _modelLayout;
|
|
||||||
private LayoutElement _chartLayout;
|
|
||||||
|
|
||||||
private enum ExpandedSide { None, Model, Chart }
|
|
||||||
private ExpandedSide _expanded = ExpandedSide.None;
|
|
||||||
|
|
||||||
private RectTransform ModelRect => modelView != null ? modelView.GetComponent<RectTransform>() : null;
|
|
||||||
private RectTransform ChartRect => chartView != null ? chartView.GetComponent<RectTransform>() : null;
|
|
||||||
private HorizontalSplitDrag _splitter;
|
|
||||||
|
|
||||||
// lifecycle
|
|
||||||
private CancellationTokenSource _cts;
|
|
||||||
private bool _suppressSelection;
|
|
||||||
|
|
||||||
// key<->id 매핑(차트-리스트/모델 동기화)
|
|
||||||
private readonly Dictionary<string, Guid> _keyToId = new Dictionary<string, Guid>();
|
|
||||||
private readonly Dictionary<Guid, string> _idToKey = new Dictionary<Guid, string>();
|
|
||||||
|
|
||||||
private Guid selectedItemId = Guid.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// UI 이벤트를 연결하고 스플리터를 준비합니다.
|
|
||||||
/// </summary>
|
|
||||||
public void Start()
|
|
||||||
{
|
|
||||||
// Close
|
|
||||||
if (closeButton != null)
|
|
||||||
{
|
|
||||||
closeButton.onClick.AddListener(OnCloseClicked);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 리스트 표시 버튼
|
|
||||||
if (showListButton != null && listView != null)
|
|
||||||
showListButton.onClick.AddListener(OnShowListClicked);
|
|
||||||
if (showListButton != null) showListButton.gameObject.SetActive(false);
|
|
||||||
|
|
||||||
// 선택 동기화: 리스트 -> 모델/차트
|
|
||||||
if (listView != null)
|
|
||||||
{
|
|
||||||
listView.OnItemSelected += OnListItemSelected;
|
|
||||||
listView.OnItemDeselected += OnListItemDeselected;
|
|
||||||
listView.OnClosed += OnListClosed;
|
|
||||||
listView.OnVisibilityChanged += OnListVisibilityChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 선택 동기화: 모델 -> 리스트/차트
|
|
||||||
if (modelView != null)
|
|
||||||
{
|
|
||||||
modelView.OnItemSelected += OnModelItemSelected;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 선택 동기화: 차트 -> 리스트/모델
|
|
||||||
if (chartView != null)
|
|
||||||
{
|
|
||||||
chartView.OnRowClickedByKey += OnChartRowClickedByKey;
|
|
||||||
chartView.OnRowClicked += OnChartRowClicked;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 확장 버튼
|
|
||||||
if (modelViewExpandButton != null)
|
|
||||||
modelViewExpandButton.onClick.AddListener(ToggleExpandModel);
|
|
||||||
if (chartViewExpandButton != null)
|
|
||||||
chartViewExpandButton.onClick.AddListener(ToggleExpandChart);
|
|
||||||
|
|
||||||
// 드래그 스플리터
|
|
||||||
SetupSplitControls();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnCloseClicked()
|
|
||||||
{
|
|
||||||
gameObject.SetActive(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnShowListClicked()
|
|
||||||
{
|
|
||||||
if (listView != null) listView.gameObject.SetActive(true);
|
|
||||||
if (showListButton != null) showListButton.gameObject.SetActive(false);
|
|
||||||
if (_splitter != null) _splitter.RefreshPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnListItemSelected(UVC.UI.List.Tree.TreeListItemData data)
|
|
||||||
{
|
|
||||||
if (data == null) return;
|
|
||||||
HandleSelection(data.Id, "ListView");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnListItemDeselected(UVC.UI.List.Tree.TreeListItemData data)
|
|
||||||
{
|
|
||||||
HandleDeselection(data.Id, "ListView");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnListClosed()
|
|
||||||
{
|
|
||||||
if (showListButton != null) showListButton.gameObject.SetActive(true);
|
|
||||||
if (_splitter != null) _splitter.RefreshPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnListVisibilityChanged(Guid id, bool vis)
|
|
||||||
{
|
|
||||||
if (modelView != null) modelView.SetVisibility(id, vis);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnModelItemSelected(UVC.UI.List.Tree.TreeListItemData data)
|
|
||||||
{
|
|
||||||
if (data == null) return;
|
|
||||||
HandleSelection(data.Id, "ModelView");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnChartRowClickedByKey(string key)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(key)) return;
|
|
||||||
if (_keyToId.TryGetValue(key, out var id)) HandleSelection(id, "ChartView");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnChartRowClicked(Guid id)
|
|
||||||
{
|
|
||||||
HandleSelection(id, "ChartView");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnClickExport()
|
|
||||||
{
|
|
||||||
if(selectedItemId != Guid.Empty && modelView != null)
|
|
||||||
{
|
|
||||||
modelView.Export(selectedItemId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// LoadData 호출 시 리스트/모델/차트를 활성화하고 분할 비율을0.5/0.5로 초기화합니다.
|
|
||||||
/// </summary>
|
|
||||||
private void InitializePanelsForLoad()
|
|
||||||
{
|
|
||||||
// 활성화 상태 설정
|
|
||||||
if (listView != null)
|
|
||||||
{
|
|
||||||
listView.gameObject.SetActive(true);
|
|
||||||
listView.Clear();
|
|
||||||
}
|
|
||||||
if (showListButton != null) showListButton.gameObject.SetActive(false);
|
|
||||||
if (ModelRect != null) ModelRect.gameObject.SetActive(true);
|
|
||||||
if (chartView != null) chartView.gameObject.SetActive(true);
|
|
||||||
_expanded = ExpandedSide.None;
|
|
||||||
|
|
||||||
// 레이아웃 요소 보장 및50/50 설정
|
|
||||||
var modelRect = ModelRect;
|
|
||||||
var chartRect = ChartRect;
|
|
||||||
if (modelRect != null)
|
|
||||||
{
|
|
||||||
_modelLayout = modelRect.GetComponent<LayoutElement>() ?? modelRect.gameObject.AddComponent<LayoutElement>();
|
|
||||||
_modelLayout.flexibleWidth = 1f;
|
|
||||||
}
|
|
||||||
if (chartRect != null)
|
|
||||||
{
|
|
||||||
_chartLayout = chartRect.GetComponent<LayoutElement>() ?? chartRect.gameObject.AddComponent<LayoutElement>();
|
|
||||||
_chartLayout.flexibleWidth = 1f;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 스플리터 보장 및 동기화
|
|
||||||
if (_splitter == null && dragButton != null && modelRect != null && chartRect != null)
|
|
||||||
{
|
|
||||||
_splitter = dragButton.gameObject.GetComponent<HorizontalSplitDrag>();
|
|
||||||
if (_splitter == null) _splitter = dragButton.gameObject.AddComponent<HorizontalSplitDrag>();
|
|
||||||
var leftFixed = listView != null ? listView.GetComponent<RectTransform>() : null;
|
|
||||||
_splitter.Initialize(modelRect, chartRect, leftFixed);
|
|
||||||
}
|
|
||||||
if (_splitter != null) _splitter.gameObject.SetActive(true);
|
|
||||||
|
|
||||||
Canvas.ForceUpdateCanvases();
|
|
||||||
_splitter?.RefreshPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// glTF 모델과 간트 데이터를 하위 뷰에 로드하고, 선택 동기화를 위한 매핑을 구축합니다.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="gltfPath">glTF/glb 파일 경로.</param>
|
|
||||||
/// <param name="ganttPath">간트 데이터셋 경로.</param>
|
|
||||||
/// <param name="externalCt">외부 취소 토큰.</param>
|
|
||||||
public async UniTask LoadData(string gltfPath, string ganttPath, CancellationToken externalCt = default)
|
|
||||||
{
|
|
||||||
Debug.Log($"BlockDetailModal: LoadData {gltfPath}");
|
|
||||||
|
|
||||||
// 레이아웃/활성 상태 초기화 (리스트/모델/차트 활성,50/50 비율)
|
|
||||||
InitializePanelsForLoad();
|
|
||||||
|
|
||||||
// 이전 작업 취소
|
|
||||||
if (_cts != null)
|
|
||||||
{
|
|
||||||
try { _cts.Cancel(); } catch { }
|
|
||||||
_cts.Dispose();
|
|
||||||
}
|
|
||||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(externalCt);
|
|
||||||
var ct = _cts.Token;
|
|
||||||
|
|
||||||
// 모델/리스트 로드
|
|
||||||
IEnumerable<ModelDetailListItemData> items = Array.Empty<ModelDetailListItemData>();
|
|
||||||
if (modelView != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
items = await modelView.LoadModelAsync(gltfPath, ct);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
BuildKeyMaps(items);
|
|
||||||
|
|
||||||
if (listView != null) listView.SetupData(items);
|
|
||||||
if (chartView != null) chartView.LoadFromStreamingAssets(ganttPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void BuildKeyMaps(IEnumerable<ModelDetailListItemData> items)
|
|
||||||
{
|
|
||||||
_keyToId.Clear();
|
|
||||||
_idToKey.Clear();
|
|
||||||
if (items == null) return;
|
|
||||||
|
|
||||||
var stack = new Stack<ModelDetailListItemData>();
|
|
||||||
foreach (var it in items)
|
|
||||||
{
|
|
||||||
if (it == null) continue;
|
|
||||||
stack.Push(it);
|
|
||||||
while (stack.Count > 0)
|
|
||||||
{
|
|
||||||
var cur = stack.Pop();
|
|
||||||
if (!string.IsNullOrEmpty(cur.ExternalKey))
|
|
||||||
{
|
|
||||||
_keyToId[cur.ExternalKey] = cur.Id;
|
|
||||||
_idToKey[cur.Id] = cur.ExternalKey;
|
|
||||||
}
|
|
||||||
if (cur.Children != null)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < cur.Children.Count; i++)
|
|
||||||
{
|
|
||||||
var child = cur.Children[i] as ModelDetailListItemData;
|
|
||||||
if (child != null) stack.Push(child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetupSplitControls()
|
|
||||||
{
|
|
||||||
var modelRect = ModelRect;
|
|
||||||
var chartRect = ChartRect;
|
|
||||||
if (modelRect == null || chartRect == null || dragButton == null) return;
|
|
||||||
|
|
||||||
_modelLayout = modelRect.GetComponent<LayoutElement>();
|
|
||||||
if (_modelLayout == null) _modelLayout = modelRect.gameObject.AddComponent<LayoutElement>();
|
|
||||||
|
|
||||||
_chartLayout = chartRect.GetComponent<LayoutElement>();
|
|
||||||
if (_chartLayout == null) _chartLayout = chartView.gameObject.AddComponent<LayoutElement>();
|
|
||||||
|
|
||||||
// 초기 분할50/50
|
|
||||||
_modelLayout.flexibleWidth = 1f;
|
|
||||||
_chartLayout.flexibleWidth = 1f;
|
|
||||||
|
|
||||||
// 드래그 핸들 부착
|
|
||||||
_splitter = dragButton.gameObject.GetComponent<HorizontalSplitDrag>();
|
|
||||||
if (_splitter == null) _splitter = dragButton.gameObject.AddComponent<HorizontalSplitDrag>();
|
|
||||||
var leftFixed = listView != null ? listView.GetComponent<RectTransform>() : null;
|
|
||||||
_splitter.Initialize(modelRect, chartRect, leftFixed);
|
|
||||||
// 레이아웃 갱신 후 위치 보정
|
|
||||||
Canvas.ForceUpdateCanvases();
|
|
||||||
_splitter.RefreshPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleSelection(Guid itemId, string source)
|
|
||||||
{
|
|
||||||
if (_suppressSelection) return;
|
|
||||||
_suppressSelection = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
selectedItemId = itemId;
|
|
||||||
if (source != "ListView" && listView != null) listView.SelectByItemId(itemId);
|
|
||||||
if (source != "ModelView" && modelView != null) modelView.FocusItemById(itemId);
|
|
||||||
if (source != "ChartView" && chartView != null)
|
|
||||||
{
|
|
||||||
//if (_idToKey.TryGetValue(itemId, out var key)) chartView.SelectByItemKey(key);
|
|
||||||
//else chartView.SelectByItemId(itemId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_suppressSelection = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleDeselection(Guid itemId, string source)
|
|
||||||
{
|
|
||||||
if (_suppressSelection) return;
|
|
||||||
if (source != "ModelView" && modelView != null) modelView.UnfocusItem();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ToggleExpandModel()
|
|
||||||
{
|
|
||||||
if (ModelRect == null || chartView == null) return;
|
|
||||||
if (_expanded == ExpandedSide.Model) { ResetSplit(); return; }
|
|
||||||
_expanded = ExpandedSide.Model;
|
|
||||||
ModelRect.gameObject.SetActive(true);
|
|
||||||
chartView.gameObject.SetActive(false);
|
|
||||||
if (_splitter != null) _splitter.gameObject.SetActive(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ToggleExpandChart()
|
|
||||||
{
|
|
||||||
if (ModelRect == null || chartView == null) return;
|
|
||||||
if (_expanded == ExpandedSide.Chart) { ResetSplit(); return; }
|
|
||||||
_expanded = ExpandedSide.Chart;
|
|
||||||
ModelRect.gameObject.SetActive(false);
|
|
||||||
chartView.gameObject.SetActive(true);
|
|
||||||
if (_splitter != null) _splitter.gameObject.SetActive(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ResetSplit()
|
|
||||||
{
|
|
||||||
_expanded = ExpandedSide.None;
|
|
||||||
if (ModelRect != null) ModelRect.gameObject.SetActive(true);
|
|
||||||
if (chartView != null) chartView.gameObject.SetActive(true);
|
|
||||||
if (_modelLayout != null) _modelLayout.flexibleWidth = 1f;
|
|
||||||
if (_chartLayout != null) _chartLayout.flexibleWidth = 1f;
|
|
||||||
if (_splitter != null)
|
|
||||||
{
|
|
||||||
_splitter.gameObject.SetActive(true);
|
|
||||||
_splitter.RefreshPosition();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDisable()
|
|
||||||
{
|
|
||||||
if (_cts != null)
|
|
||||||
{
|
|
||||||
try { _cts.Cancel(); } catch { }
|
|
||||||
_cts.Dispose();
|
|
||||||
_cts = null;
|
|
||||||
}
|
|
||||||
if (chartView != null) chartView.Dispose();
|
|
||||||
if (modelView != null) modelView.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDestroy()
|
|
||||||
{
|
|
||||||
// 이벤트 해제(메모리 누수 방지)
|
|
||||||
if (closeButton != null) closeButton.onClick.RemoveListener(OnCloseClicked);
|
|
||||||
if (showListButton != null && listView != null) showListButton.onClick.RemoveListener(OnShowListClicked);
|
|
||||||
if (modelViewExpandButton != null) modelViewExpandButton.onClick.RemoveListener(ToggleExpandModel);
|
|
||||||
if (chartViewExpandButton != null) chartViewExpandButton.onClick.RemoveListener(ToggleExpandChart);
|
|
||||||
|
|
||||||
if (listView != null)
|
|
||||||
{
|
|
||||||
listView.OnItemSelected -= OnListItemSelected;
|
|
||||||
listView.OnItemDeselected -= OnListItemDeselected;
|
|
||||||
listView.OnClosed -= OnListClosed;
|
|
||||||
listView.OnVisibilityChanged -= OnListVisibilityChanged;
|
|
||||||
}
|
|
||||||
if (modelView != null)
|
|
||||||
{
|
|
||||||
modelView.OnItemSelected -= OnModelItemSelected;
|
|
||||||
}
|
|
||||||
if (chartView != null)
|
|
||||||
{
|
|
||||||
chartView.OnRowClickedByKey -= OnChartRowClickedByKey;
|
|
||||||
chartView.OnRowClicked -= OnChartRowClicked;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_cts != null)
|
|
||||||
{
|
|
||||||
try { _cts.Cancel(); } catch { }
|
|
||||||
_cts.Dispose();
|
|
||||||
_cts = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: daf98703dca342f458a0cf5df71adddb
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
#nullable enable
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace SHI.modal
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 간트 차트의 한 구간(세그먼트) 정보를 나타냅니다.
|
|
||||||
/// </summary>
|
|
||||||
public class ScheduleSegment
|
|
||||||
{
|
|
||||||
/// <summary>외부 연동용 안정 키(권장).</summary>
|
|
||||||
public string ItemKey { get; set; } = string.Empty;
|
|
||||||
/// <summary>호환용 GUID(선택적).</summary>
|
|
||||||
public Guid ItemId { get; set; }
|
|
||||||
/// <summary>시작 시각(UTC 권장).</summary>
|
|
||||||
public DateTime Start { get; set; }
|
|
||||||
/// <summary>종료 시각(UTC 권장).</summary>
|
|
||||||
public DateTime End { get; set; }
|
|
||||||
/// <summary>진행률 값([0..1] 또는 [0..100] 등 상위 시스템 규약 따름).</summary>
|
|
||||||
public float Progress { get; set; }
|
|
||||||
/// <summary>유형/카테고리 등 사용자 지정 문자열.</summary>
|
|
||||||
public string Type { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 기간/마커 정보(Structured 행 모드용).
|
|
||||||
/// </summary>
|
|
||||||
public class DateRange
|
|
||||||
{
|
|
||||||
public DateTime? Start { get; set; }
|
|
||||||
public DateTime? End { get; set; }
|
|
||||||
public int? Duration { get; set; }
|
|
||||||
public bool IsMarker => Start.HasValue && !End.HasValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 블록/행 단위 일정 레코드.
|
|
||||||
/// </summary>
|
|
||||||
public class BlockScheduleRow
|
|
||||||
{
|
|
||||||
public string? ProjNo { get; set; }
|
|
||||||
public string? BlockNo { get; set; }
|
|
||||||
public string? L1 { get; set; }
|
|
||||||
public string? L2 { get; set; }
|
|
||||||
public string? L3 { get; set; }
|
|
||||||
public string? L4 { get; set; }
|
|
||||||
public string? L5 { get; set; }
|
|
||||||
public string? L6 { get; set; }
|
|
||||||
public string? L7 { get; set; }
|
|
||||||
public string? L8 { get; set; }
|
|
||||||
/// <summary>코드(예:43,4B 등)별 기간/마커.</summary>
|
|
||||||
public Dictionary<string, DateRange> Periods { get; } = new Dictionary<string, DateRange>();
|
|
||||||
// 추가 메타 정보(JSON 컬럼 매핑)
|
|
||||||
public string? BlkDsc { get; set; } // BLK_DSC
|
|
||||||
public string? ShipType { get; set; } // SHIP_TYPE
|
|
||||||
public string? ShrGb { get; set; } // SHR_GB
|
|
||||||
public float? Lth { get; set; } // LTH
|
|
||||||
public float? Bth { get; set; } // BTH
|
|
||||||
public float? Hgt { get; set; } // HGT
|
|
||||||
public float? NetWgt { get; set; } // NET_WGT
|
|
||||||
public float? EqupWgt { get; set; } // EQUP_WGT
|
|
||||||
public float? LftWgt { get; set; } // LFT_WGT
|
|
||||||
public string? WkType { get; set; } // WK_TYPE
|
|
||||||
public string? Wc31 { get; set; } // WC31
|
|
||||||
|
|
||||||
// 편의 접근자: 자주 쓰는 코드 맵핑
|
|
||||||
/// <summary>STDT01: 파란색 @ 표시 일(yyyymmdd)</summary>
|
|
||||||
public DateTime? STDT01 { get => GetStart("01"); set => SetStart("01", value); }
|
|
||||||
/// <summary>STDT21: 파란색 막대 시작일(yyyymmdd)</summary>
|
|
||||||
public DateTime? STDT21 { get => GetStart("21"); set => SetStart("21", value); }
|
|
||||||
/// <summary>FNDT21: 파란색 막대 종료일(yyyymmdd)</summary>
|
|
||||||
public DateTime? FNDT21 { get => GetEnd("21"); set => SetEnd("21", value); }
|
|
||||||
/// <summary>STDT23: 연두색 막대 시작일(yyyymmdd)</summary>
|
|
||||||
public DateTime? STDT23 { get => GetStart("23"); set => SetStart("23", value); }
|
|
||||||
/// <summary>FNDT23: 연두색 막대 종료일(yyyymmdd)</summary>
|
|
||||||
public DateTime? FNDT23 { get => GetEnd("23"); set => SetEnd("23", value); }
|
|
||||||
|
|
||||||
private DateRange GetOrCreateRange(string code)
|
|
||||||
{
|
|
||||||
if (!Periods.TryGetValue(code, out var dr) || dr == null)
|
|
||||||
{
|
|
||||||
dr = new DateRange();
|
|
||||||
Periods[code] = dr;
|
|
||||||
}
|
|
||||||
return dr;
|
|
||||||
}
|
|
||||||
private DateTime? GetStart(string code) => Periods.TryGetValue(code, out var dr) ? dr.Start : null;
|
|
||||||
private DateTime? GetEnd(string code) => Periods.TryGetValue(code, out var dr) ? dr.End : null;
|
|
||||||
private void SetStart(string code, DateTime? v) { var dr = GetOrCreateRange(code); dr.Start = v; }
|
|
||||||
private void SetEnd(string code, DateTime? v) { var dr = GetOrCreateRange(code); dr.End = v; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 간단한 간트 차트 데이터셋입니다.
|
|
||||||
/// 기존 Segments 기반(Flat) + Rows 기반(Structured) 동시 지원.
|
|
||||||
/// </summary>
|
|
||||||
public class GanttChartData
|
|
||||||
{
|
|
||||||
/// <summary>표시 순서대로의 세그먼트 컬렉션 (Flat 모드).</summary>
|
|
||||||
public List<ScheduleSegment> Segments { get; set; } = new List<ScheduleSegment>();
|
|
||||||
/// <summary>Structured 행 모드 컬렉션.</summary>
|
|
||||||
public List<BlockScheduleRow> Rows { get; set; } = new List<BlockScheduleRow>();
|
|
||||||
public DateTime? MinDate { get; set; }
|
|
||||||
public DateTime? MaxDate { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 5ec1e373110ba694f916a50093c32919
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
// GanttJsonParser: 행 기반(STDTxx/FNDTxx/DURxx) 간트 데이터 파서
|
|
||||||
// 요구: 오류가 있어도 삭제하지 말 것.
|
|
||||||
#nullable enable
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace SHI.modal
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// isop_chart.json과 같은 행 배열(딕셔너리)의 STDTxx / FNDTxx / DURxx 패턴을 해석하여 <see cref="GanttChartData"/>를 생성합니다.
|
|
||||||
/// </summary>
|
|
||||||
public static class GanttJsonParser
|
|
||||||
{
|
|
||||||
private static readonly Regex StartRegex = new Regex("^STDT([0-9A-FM]{2})$", RegexOptions.Compiled);
|
|
||||||
private static readonly Regex EndRegex = new Regex("^FNDT([0-9A-FM]{2})$", RegexOptions.Compiled);
|
|
||||||
private static readonly Regex DurRegex = new Regex("^DUR([0-9A-FM]{2})$", RegexOptions.Compiled);
|
|
||||||
private const string DateFormat = "yyyyMMdd";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// JSON 배열 문자열을 파싱해 Rows/Segments/MinDate/MaxDate를 채웁니다.
|
|
||||||
/// Duration 누락 시 (End-Start)+1을 사용합니다.
|
|
||||||
/// </summary>
|
|
||||||
public static GanttChartData Parse(string json, bool computeDurationIfMissing = true)
|
|
||||||
{
|
|
||||||
var data = new GanttChartData();
|
|
||||||
if (string.IsNullOrWhiteSpace(json)) return data;
|
|
||||||
|
|
||||||
List<Dictionary<string, object>>? rows;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Newtonsoft.Json 사용 (Assembly 내 참조 필요). 실패 시 경고 후 빈 데이터 반환.
|
|
||||||
rows = Newtonsoft.Json.JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(json);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.LogWarning($"GanttJsonParser.Parse: JSON deserialize failed: {ex.Message}");
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
if (rows == null) return data;
|
|
||||||
|
|
||||||
DateTime? min = null; DateTime? max = null;
|
|
||||||
|
|
||||||
foreach (var raw in rows)
|
|
||||||
{
|
|
||||||
if (raw == null) continue;
|
|
||||||
var row = new BlockScheduleRow
|
|
||||||
{
|
|
||||||
ProjNo = GetStr(raw, "PROJ_NO"),
|
|
||||||
BlockNo = GetStr(raw, "BLK_NO"),
|
|
||||||
L1 = GetStr(raw, "L1"),
|
|
||||||
L2 = GetStr(raw, "L2"),
|
|
||||||
L3 = GetStr(raw, "L3"),
|
|
||||||
L4 = GetStr(raw, "L4"),
|
|
||||||
L5 = GetStr(raw, "L5"),
|
|
||||||
L6 = GetStr(raw, "L6"),
|
|
||||||
L7 = GetStr(raw, "L7"),
|
|
||||||
L8 = GetStr(raw, "L8"),
|
|
||||||
// 추가 메타
|
|
||||||
BlkDsc = GetStr(raw, "BLK_DSC"),
|
|
||||||
ShipType = GetStr(raw, "SHIP_TYPE"),
|
|
||||||
ShrGb = GetStr(raw, "SHR_GB"),
|
|
||||||
Lth = GetFloat(raw, "LTH"),
|
|
||||||
Bth = GetFloat(raw, "BTH"),
|
|
||||||
Hgt = GetFloat(raw, "HGT"),
|
|
||||||
NetWgt = GetFloat(raw, "NET_WGT"),
|
|
||||||
EqupWgt = GetFloat(raw, "EQUP_WGT"),
|
|
||||||
LftWgt = GetFloat(raw, "LFT_WGT"),
|
|
||||||
WkType = GetStr(raw, "WK_TYPE"),
|
|
||||||
Wc31 = GetStr(raw, "WC31"),
|
|
||||||
};
|
|
||||||
|
|
||||||
var startMap = new Dictionary<string, DateTime>();
|
|
||||||
var endMap = new Dictionary<string, DateTime>();
|
|
||||||
var durMap = new Dictionary<string, int>();
|
|
||||||
|
|
||||||
foreach (var kv in raw)
|
|
||||||
{
|
|
||||||
if (kv.Key == null || kv.Value == null) continue;
|
|
||||||
var val = kv.Value.ToString();
|
|
||||||
if (string.IsNullOrWhiteSpace(val)) continue;
|
|
||||||
|
|
||||||
var sm = StartRegex.Match(kv.Key);
|
|
||||||
if (sm.Success)
|
|
||||||
{
|
|
||||||
if (TryParseDate(val, out var sdt))
|
|
||||||
{
|
|
||||||
var code = sm.Groups[1].Value;
|
|
||||||
if (startMap.ContainsKey(code)) Debug.Log($"GanttJsonParser: duplicate STDT for code={code}, last value wins.");
|
|
||||||
startMap[code] = sdt; UpdateMinMax(ref min, ref max, sdt); continue;
|
|
||||||
}
|
|
||||||
else { Debug.LogWarning($"GanttJsonParser: start date parse failed key={kv.Key} value={val}"); continue; }
|
|
||||||
}
|
|
||||||
var em = EndRegex.Match(kv.Key);
|
|
||||||
if (em.Success)
|
|
||||||
{
|
|
||||||
if (TryParseDate(val, out var edt))
|
|
||||||
{
|
|
||||||
var code = em.Groups[1].Value;
|
|
||||||
if (endMap.ContainsKey(code)) Debug.Log($"GanttJsonParser: duplicate FNDT for code={code}, last value wins.");
|
|
||||||
endMap[code] = edt; UpdateMinMax(ref min, ref max, edt); continue;
|
|
||||||
}
|
|
||||||
else { Debug.LogWarning($"GanttJsonParser: end date parse failed key={kv.Key} value={val}"); continue; }
|
|
||||||
}
|
|
||||||
var dm = DurRegex.Match(kv.Key);
|
|
||||||
if (dm.Success)
|
|
||||||
{
|
|
||||||
var code = dm.Groups[1].Value;
|
|
||||||
if (int.TryParse(val, out var dur))
|
|
||||||
{
|
|
||||||
if (durMap.ContainsKey(code)) Debug.Log($"GanttJsonParser: duplicate DUR for code={code}, last value wins.");
|
|
||||||
durMap[code] = dur; continue;
|
|
||||||
}
|
|
||||||
else { Debug.LogWarning($"GanttJsonParser: duration parse failed key={kv.Key} value={val}"); continue; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var codes = new HashSet<string>(startMap.Keys);
|
|
||||||
codes.UnionWith(endMap.Keys);
|
|
||||||
codes.UnionWith(durMap.Keys);
|
|
||||||
|
|
||||||
foreach (var code in codes)
|
|
||||||
{
|
|
||||||
var range = new DateRange();
|
|
||||||
if (startMap.TryGetValue(code, out var s)) range.Start = s;
|
|
||||||
if (endMap.TryGetValue(code, out var e)) range.End = e;
|
|
||||||
if (durMap.TryGetValue(code, out var d)) range.Duration = d;
|
|
||||||
else if (computeDurationIfMissing && range.Start.HasValue && range.End.HasValue)
|
|
||||||
range.Duration = (int)(range.End.Value.Date - range.Start.Value.Date).TotalDays + 1;
|
|
||||||
|
|
||||||
// Start > End 정책: Skip + Warning
|
|
||||||
if (range.Start.HasValue && range.End.HasValue && range.End.Value.Date < range.Start.Value.Date)
|
|
||||||
{
|
|
||||||
Debug.LogWarning($"GanttJsonParser: Start > End. Skip record block={row.BlockNo} code={code} start={range.Start:yyyyMMdd} end={range.End:yyyyMMdd}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
row.Periods[code] = range;
|
|
||||||
|
|
||||||
if (range.Start.HasValue && range.End.HasValue)
|
|
||||||
{
|
|
||||||
// Flat segment 동시 생성 (유효한 경우만)
|
|
||||||
data.Segments.Add(new ScheduleSegment
|
|
||||||
{
|
|
||||||
ItemKey = (row.BlockNo ?? string.Empty) + "|" + code,
|
|
||||||
ItemId = Guid.Empty,
|
|
||||||
Start = range.Start.Value,
|
|
||||||
End = range.End.Value,
|
|
||||||
Progress = 0f,
|
|
||||||
Type = code
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data.Rows.Add(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
data.MinDate = min; data.MaxDate = max;
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void UpdateMinMax(ref DateTime? min, ref DateTime? max, DateTime v)
|
|
||||||
{
|
|
||||||
if (!min.HasValue || v < min.Value) min = v;
|
|
||||||
if (!max.HasValue || v > max.Value) max = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryParseDate(string s, out DateTime dt)
|
|
||||||
=> DateTime.TryParseExact(s, DateFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out dt);
|
|
||||||
|
|
||||||
private static string? GetStr(Dictionary<string, object> dict, string key)
|
|
||||||
=> dict.TryGetValue(key, out var v) ? v?.ToString() : null;
|
|
||||||
|
|
||||||
private static float? GetFloat(Dictionary<string, object> dict, string key)
|
|
||||||
{
|
|
||||||
if (!dict.TryGetValue(key, out var v) || v == null) return null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
switch (v)
|
|
||||||
{
|
|
||||||
case float f: return f;
|
|
||||||
case double d: return (float)d;
|
|
||||||
case decimal m: return (float)m;
|
|
||||||
case int i: return i;
|
|
||||||
case long l: return l;
|
|
||||||
case short s: return s;
|
|
||||||
case string str:
|
|
||||||
if (string.IsNullOrWhiteSpace(str)) return null;
|
|
||||||
if (float.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out var pf)) return pf;
|
|
||||||
if (float.TryParse(str, NumberStyles.Any, CultureInfo.CurrentCulture, out pf)) return pf;
|
|
||||||
return null;
|
|
||||||
default:
|
|
||||||
// 마지막 시도: System.Convert
|
|
||||||
return (float)Convert.ToDouble(v, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { return null; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 0472849d1587ac84491dbf3f1b4c68ee
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
#nullable enable
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.EventSystems;
|
|
||||||
using UnityEngine.UI;
|
|
||||||
|
|
||||||
namespace SHI.modal
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 수평 레이아웃에서 두 패널의 가중치(LayoutElement.flexibleWidth)를 드래그 핸들로 조절하는 간단한 분할기입니다.
|
|
||||||
/// </summary>
|
|
||||||
public class HorizontalSplitDrag : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
|
|
||||||
{
|
|
||||||
[SerializeField, Range(0.0f, 0.5f)] private float minLeftWeight = 0.1f;
|
|
||||||
[SerializeField, Range(0.5f, 1.0f)] private float maxLeftWeight = 0.9f;
|
|
||||||
[SerializeField] private bool useActualWidthForHandle = true; // 실제 패널 폭 기준 위치 보정
|
|
||||||
|
|
||||||
private RectTransform _left;
|
|
||||||
private RectTransform _right;
|
|
||||||
private LayoutElement _leftLayout;
|
|
||||||
private LayoutElement _rightLayout;
|
|
||||||
private RectTransform _parent;
|
|
||||||
private RectTransform _handleRect; // this splitter's rect
|
|
||||||
private float _parentWidth; // cached on begin drag
|
|
||||||
private float _handleY = 42; // keep original y
|
|
||||||
private RectTransform _leftFixedPanel; // e.g., ModelDetailListView root
|
|
||||||
|
|
||||||
private bool _lastLeftPanelActive;
|
|
||||||
private float _lastParentWidth;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 좌/우 패널을 지정하고(필요 시 좌측 고정 패널 포함) 분할기를 초기화합니다.
|
|
||||||
/// </summary>
|
|
||||||
public void Initialize(RectTransform left, RectTransform right, RectTransform? leftFixedPanel = null)
|
|
||||||
{
|
|
||||||
_left = left;
|
|
||||||
_right = right;
|
|
||||||
_leftFixedPanel = leftFixedPanel;
|
|
||||||
_parent = left != null ? left.parent as RectTransform : null;
|
|
||||||
_handleRect = transform as RectTransform;
|
|
||||||
|
|
||||||
_leftLayout = _left != null ? _left.GetComponent<LayoutElement>() : null;
|
|
||||||
if (_left != null && _leftLayout == null) _leftLayout = _left.gameObject.AddComponent<LayoutElement>();
|
|
||||||
_rightLayout = _right != null ? _right.GetComponent<LayoutElement>() : null;
|
|
||||||
if (_right != null && _rightLayout == null) _rightLayout = _right.gameObject.AddComponent<LayoutElement>();
|
|
||||||
|
|
||||||
_lastLeftPanelActive = _leftFixedPanel != null && _leftFixedPanel.gameObject.activeInHierarchy;
|
|
||||||
_lastParentWidth = _parent != null ? _parent.rect.width : 0f;
|
|
||||||
RefreshPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void OnBeginDrag(PointerEventData eventData)
|
|
||||||
{
|
|
||||||
if (_parent != null) _parentWidth = _parent.rect.width;
|
|
||||||
if (_handleRect != null) _handleY = _handleRect.anchoredPosition.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 부모 RectTransform pivot을 고려한 작업 영역 좌/우 경계 계산.
|
|
||||||
/// </summary>
|
|
||||||
private void GetWorkArea(out float minX, out float maxX, out float leftOffset)
|
|
||||||
{
|
|
||||||
minX = maxX = leftOffset = 0f;
|
|
||||||
if (_parent == null) return;
|
|
||||||
float width = _parent.rect.width;
|
|
||||||
var pivot = _parent.pivot; // (0..1)
|
|
||||||
float pivotOrigin = -width * pivot.x; // local 좌표계에서 좌측 경계
|
|
||||||
leftOffset = 0f;
|
|
||||||
if (_leftFixedPanel != null && _leftFixedPanel.gameObject.activeSelf)
|
|
||||||
leftOffset = _leftFixedPanel.rect.width; // 고정 패널 폭만큼 이동
|
|
||||||
minX = pivotOrigin + leftOffset;
|
|
||||||
maxX = pivotOrigin + width; // 우측 경계 (handle 중심 좌표)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void OnDrag(PointerEventData eventData)
|
|
||||||
{
|
|
||||||
if (_parent == null || _leftLayout == null || _rightLayout == null) return;
|
|
||||||
Vector2 local;
|
|
||||||
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(_parent, eventData.position, eventData.pressEventCamera, out local))
|
|
||||||
return;
|
|
||||||
float width = _parent.rect.width;
|
|
||||||
if (width <= 0f) return;
|
|
||||||
|
|
||||||
float minX, maxX, leftOffset;
|
|
||||||
GetWorkArea(out minX, out maxX, out leftOffset);
|
|
||||||
|
|
||||||
// 드래그 포인터를 작업 영역으로 정규화하여 [0..1] 비율 t 산출
|
|
||||||
float t = Mathf.InverseLerp(minX, maxX, local.x);
|
|
||||||
t = Mathf.Clamp01(t);
|
|
||||||
|
|
||||||
// 가변 비율 계산 (minLeftWeight~maxLeftWeight 범위)
|
|
||||||
float leftWeight = Mathf.Lerp(minLeftWeight, maxLeftWeight, t);
|
|
||||||
float rightWeight = Mathf.Max(0.0001f, 1f - leftWeight);
|
|
||||||
_leftLayout.flexibleWidth = leftWeight;
|
|
||||||
_rightLayout.flexibleWidth = rightWeight;
|
|
||||||
|
|
||||||
// 레이아웃 즉시 반영 후 실제 폭 기준 위치 재계산
|
|
||||||
Canvas.ForceUpdateCanvases();
|
|
||||||
UpdateHandlePositionFromActualWidths(minX, maxX);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 실제 패널 폭을 사용해 핸들 위치를 경계선(왼쪽 패널 오른쪽 끝)에 정렬.
|
|
||||||
/// 레이아웃 그룹 padding/spacing, 고정 패널 폭을 모두 반영.
|
|
||||||
/// </summary>
|
|
||||||
private void UpdateHandlePositionFromActualWidths(float minX, float maxX)
|
|
||||||
{
|
|
||||||
if (_handleRect == null || _parent == null || _left == null) return;
|
|
||||||
if (!useActualWidthForHandle)
|
|
||||||
{
|
|
||||||
// 기존 비율 방식 (보정 없이)
|
|
||||||
float totalFlex = Mathf.Max(0.0001f, _leftLayout.flexibleWidth + _rightLayout.flexibleWidth);
|
|
||||||
float leftWeight = Mathf.Clamp01(_leftLayout.flexibleWidth / totalFlex);
|
|
||||||
float normalized = Mathf.InverseLerp(minLeftWeight, maxLeftWeight, Mathf.Clamp(leftWeight, minLeftWeight, maxLeftWeight));
|
|
||||||
float xRatio = Mathf.Lerp(minX, maxX, normalized);
|
|
||||||
_handleRect.anchoredPosition = new Vector2(xRatio, _handleY);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 고정 패널 폭 + 가변 왼쪽 패널 실제 폭 = 경계 픽셀
|
|
||||||
float fixedWidth = (_leftFixedPanel != null && _leftFixedPanel.gameObject.activeSelf) ? _leftFixedPanel.rect.width : 0f;
|
|
||||||
float variableLeftWidth = _left.rect.width; // 레이아웃 적용 후 실제 계산된 폭
|
|
||||||
|
|
||||||
// 부모 pivot 기반 local 좌표 좌측 경계
|
|
||||||
float widthParent = _parent.rect.width;
|
|
||||||
float origin = -widthParent * _parent.pivot.x;
|
|
||||||
float boundaryX = origin + fixedWidth + variableLeftWidth;
|
|
||||||
|
|
||||||
// 핸들 자체 폭 중앙 정렬 (핸들 pivot이 중앙이라고 가정)
|
|
||||||
// 경계 근처에서 과도한 이동 방지 (clamp)
|
|
||||||
float halfHandle = _handleRect.rect.width * 0.5f;
|
|
||||||
float minCenter = origin + fixedWidth + halfHandle; // 최소 중앙: 고정 패널 끝 + 반 핸들
|
|
||||||
float maxCenter = origin + widthParent - halfHandle; // 최대 중앙: 부모 우측 - 반 핸들
|
|
||||||
float centerX = Mathf.Clamp(boundaryX, minCenter, maxCenter);
|
|
||||||
_handleRect.anchoredPosition = new Vector2(centerX, _handleY);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 현재 레이아웃 상태에 맞게 드래그 핸들의 위치를 동기화합니다.
|
|
||||||
/// </summary>
|
|
||||||
public void RefreshPosition()
|
|
||||||
{
|
|
||||||
if (_parent == null || _handleRect == null || _leftLayout == null || _rightLayout == null) return;
|
|
||||||
float width = _parent.rect.width;
|
|
||||||
if (width <= 0f) return;
|
|
||||||
|
|
||||||
float minX, maxX, leftOffset;
|
|
||||||
GetWorkArea(out minX, out maxX, out leftOffset);
|
|
||||||
|
|
||||||
Canvas.ForceUpdateCanvases();
|
|
||||||
UpdateHandlePositionFromActualWidths(minX, maxX);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void LateUpdate()
|
|
||||||
{
|
|
||||||
bool nowActive = _leftFixedPanel != null && _leftFixedPanel.gameObject.activeInHierarchy;
|
|
||||||
float nowWidth = _parent != null ? _parent.rect.width : 0f;
|
|
||||||
if (nowActive != _lastLeftPanelActive || !Mathf.Approximately(nowWidth, _lastParentWidth))
|
|
||||||
{
|
|
||||||
_lastLeftPanelActive = nowActive;
|
|
||||||
_lastParentWidth = nowWidth;
|
|
||||||
RefreshPosition();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void OnEndDrag(PointerEventData eventData) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 4cdfc0facccb5164e87bd49a9a9be1e3
|
|
||||||
@@ -1,510 +0,0 @@
|
|||||||
#nullable enable
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.UIElements;
|
|
||||||
|
|
||||||
namespace SHI.modal
|
|
||||||
{
|
|
||||||
public class ModelDetailChartView2 : MonoBehaviour
|
|
||||||
{
|
|
||||||
public Action<string>? OnRowClickedByKey;
|
|
||||||
public Action<Guid>? OnRowClicked;
|
|
||||||
private GanttChartData? _data;
|
|
||||||
[SerializeField] private UIDocument? uiDocument; // UI Toolkit 루트 문서
|
|
||||||
[SerializeField] private string chartRootName = "gantt-root"; // 동적 삽입 컨테이너 이름
|
|
||||||
[SerializeField] private float dayWidth = 16f;
|
|
||||||
[SerializeField] private int rowHeight = 24;
|
|
||||||
[SerializeField] private StyleSheet? defaultStyleSheet; // USS 연결용 (Inspector 가능)
|
|
||||||
[SerializeField] private bool showTodayLine = false;
|
|
||||||
[SerializeField] private bool logAlignmentDebug = false; // 위치 동기화 디버그 로그
|
|
||||||
|
|
||||||
// 선택 상태 관리
|
|
||||||
private readonly Dictionary<string, VisualElement> _rowElements = new Dictionary<string, VisualElement>();
|
|
||||||
private readonly Dictionary<string, VisualElement> _segmentElements = new Dictionary<string, VisualElement>();
|
|
||||||
private string? _selectedKey;
|
|
||||||
private string? _selectedSegmentKey;
|
|
||||||
|
|
||||||
// 필터 상태
|
|
||||||
private DateTime? _filterFrom;
|
|
||||||
private DateTime? _filterTo;
|
|
||||||
private string? _filterCode;
|
|
||||||
private string? _filterBlockNo;
|
|
||||||
|
|
||||||
// 헤더 스크롤 동기화 참조
|
|
||||||
private VisualElement? _monthRowRef;
|
|
||||||
private VisualElement? _weekRowRef;
|
|
||||||
private VisualElement? _dayRowRef;
|
|
||||||
private ScrollView? _bodyScrollRef;
|
|
||||||
|
|
||||||
public void LoadFromStreamingAssets(string fileName = "isop_chart.json")
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Ensure a UIDocument exists so we can render
|
|
||||||
EnsureUIDocument();
|
|
||||||
|
|
||||||
var path = Path.Combine(Application.streamingAssetsPath, fileName);
|
|
||||||
if (!File.Exists(path))
|
|
||||||
{
|
|
||||||
Debug.LogError($"File not found: {path}");
|
|
||||||
// 더미 데이터 로드 정책
|
|
||||||
_data = CreateDummyData();
|
|
||||||
BuildStubUI();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//5MB 초과 경고
|
|
||||||
try
|
|
||||||
{ var fi = new FileInfo(path); if (fi.Exists && fi.Length > 5 * 1024 * 1024) Debug.LogWarning($"Chart JSON is large ({fi.Length / (1024 * 1024f):F1} MB). Consider streaming parser."); }
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
var json = File.ReadAllText(path);
|
|
||||||
|
|
||||||
// 행 기반(STDT/FNDT/DUR) 시도
|
|
||||||
var structured = TryParseStructured(json);
|
|
||||||
if (structured != null)
|
|
||||||
{
|
|
||||||
LoadData(structured);
|
|
||||||
BuildStubUI();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Debug.LogWarning("Unsupported chart JSON format.");
|
|
||||||
}
|
|
||||||
catch (Exception ex) { Debug.LogError($"LoadFromStreamingAssets failed: {ex.Message}"); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LoadData(GanttChartData data)
|
|
||||||
{
|
|
||||||
_data = data;
|
|
||||||
Debug.Log($"ChartView.LoadData: rows={data.Rows.Count} seg={data.Segments.Count} range={data.MinDate}->{data.MaxDate}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SelectByItemKey(string key)
|
|
||||||
{
|
|
||||||
if (_data == null) { Debug.Log("SelectByItemKey: no data"); return; }
|
|
||||||
_selectedKey = key ?? string.Empty;
|
|
||||||
ApplySelectionVisuals();
|
|
||||||
}
|
|
||||||
public void SelectByItemId(Guid id)
|
|
||||||
{ if (_data == null) { Debug.Log("SelectByItemId: no data"); return; } Debug.Log($"Highlight row id={id}"); }
|
|
||||||
public void SimulateRowClickKey(string key) => OnRowClickedByKey?.Invoke(key);
|
|
||||||
public void SimulateRowClick(string id) { if (Guid.TryParse(id, out var g)) OnRowClicked?.Invoke(g); }
|
|
||||||
public void Dispose() { _data = null; ClearUI(); _rowElements.Clear(); _segmentElements.Clear(); _selectedKey = null; _selectedSegmentKey = null; }
|
|
||||||
public void ToggleTodayLine(bool enabled) { showTodayLine = enabled; BuildStubUI(); }
|
|
||||||
public void SetZoom(float newDayWidth) { var clamped = Mathf.Clamp(newDayWidth, 8f, 32f); if (Mathf.Approximately(clamped, dayWidth)) return; dayWidth = clamped; BuildStubUI(); }
|
|
||||||
public void ApplyFilter(DateTime? from, DateTime? to, string? code = null, string? blockNo = null)
|
|
||||||
{ _filterFrom = from; _filterTo = to; _filterCode = string.IsNullOrEmpty(code) ? null : code; _filterBlockNo = string.IsNullOrEmpty(blockNo) ? null : blockNo; BuildStubUI(); }
|
|
||||||
|
|
||||||
private void ClearUI()
|
|
||||||
{
|
|
||||||
if (!EnsureUIDocument()) return;
|
|
||||||
var root = uiDocument!.rootVisualElement?.Q(chartRootName);
|
|
||||||
root?.Clear();
|
|
||||||
_rowElements.Clear();
|
|
||||||
_segmentElements.Clear();
|
|
||||||
_monthRowRef = _weekRowRef = _dayRowRef = null;
|
|
||||||
_bodyScrollRef = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private GanttChartData? TryParseStructured(string json)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var data = GanttJsonParser.Parse(json, computeDurationIfMissing: true);
|
|
||||||
// 구조적 파싱 성공 조건: 최소 한 행 또는 최소 한 기간
|
|
||||||
if (data != null && (data.Rows.Count > 0 || data.Segments.Count > 0))
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.LogWarning($"Structured parse failed: {ex.Message}");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 간단 렌더(헤더+행 막대) 스텁
|
|
||||||
private void BuildStubUI()
|
|
||||||
{
|
|
||||||
if (!EnsureUIDocument()) { Debug.Log("BuildStubUI: UIDocument missing and could not be created"); return; }
|
|
||||||
if (_data == null) { Debug.Log("BuildStubUI: no data"); return; }
|
|
||||||
var root = uiDocument!.rootVisualElement;
|
|
||||||
root.style.backgroundColor = StyleKeyword.Null; // 전체 패널은 투명 유지
|
|
||||||
var container = root.Q(chartRootName) ?? new VisualElement { name = chartRootName };
|
|
||||||
if (container.parent == null) root.Add(container);
|
|
||||||
container.Clear();
|
|
||||||
container.style.flexDirection = FlexDirection.Column;
|
|
||||||
container.style.position = Position.Absolute;
|
|
||||||
container.style.backgroundColor = Color.white; // 뷰 영역만 흰색
|
|
||||||
container.style.overflow = Overflow.Hidden; // 뷰 밖으로 나가는 내용 클리핑
|
|
||||||
EnsureStyleSheet(container);
|
|
||||||
|
|
||||||
// Sync size and position to RectTransform
|
|
||||||
SyncContainerSize();
|
|
||||||
SyncContainerPosition();
|
|
||||||
|
|
||||||
var min = _data.MinDate ?? DateTime.Today;
|
|
||||||
var max = _data.MaxDate ?? min;
|
|
||||||
var totalDays = Math.Max(1, (int)(max.Date - min.Date).TotalDays + 1);
|
|
||||||
var timelineWidth = totalDays * dayWidth;
|
|
||||||
|
|
||||||
// Header container (Month/Week/Day)
|
|
||||||
var header = new VisualElement { name = "gantt-header" };
|
|
||||||
header.AddToClassList("header-container");
|
|
||||||
header.style.flexDirection = FlexDirection.Column;
|
|
||||||
header.style.backgroundColor = new Color(0.13f, 0.13f, 0.15f, 1f);
|
|
||||||
header.style.height = 60;
|
|
||||||
header.style.position = Position.Relative;
|
|
||||||
header.style.width = timelineWidth;
|
|
||||||
BuildHeaderRows(header, min, max, totalDays, showTodayLine);
|
|
||||||
container.Add(header);
|
|
||||||
|
|
||||||
_rowElements.Clear(); _segmentElements.Clear();
|
|
||||||
|
|
||||||
// Body scroll
|
|
||||||
var bodyScroll = new ScrollView(ScrollViewMode.VerticalAndHorizontal) { name = "body-scroll" };
|
|
||||||
bodyScroll.contentContainer.style.flexDirection = FlexDirection.Column;
|
|
||||||
container.Add(bodyScroll);
|
|
||||||
_bodyScrollRef = bodyScroll;
|
|
||||||
|
|
||||||
var rowsContainer = new VisualElement { name = "rows-container" };
|
|
||||||
rowsContainer.AddToClassList("rows-container");
|
|
||||||
rowsContainer.style.flexDirection = FlexDirection.Column;
|
|
||||||
rowsContainer.style.width = timelineWidth; // 수평 스크롤 기준 폭
|
|
||||||
bodyScroll.Add(rowsContainer);
|
|
||||||
|
|
||||||
// Rows만 렌더 (Segments Fallback 제거)
|
|
||||||
foreach (var row in _data.Rows)
|
|
||||||
{
|
|
||||||
if (_filterBlockNo != null && !string.Equals(row.BlockNo ?? string.Empty, _filterBlockNo, StringComparison.OrdinalIgnoreCase)) { bool hasAlt = false; if (!hasAlt) continue; }
|
|
||||||
BuildRow(rowsContainer, row.BlockNo ?? row.L1 ?? "", min, timelineWidth, row);
|
|
||||||
}
|
|
||||||
|
|
||||||
HookHeaderScrollSync();
|
|
||||||
// 선택 상태 반영
|
|
||||||
ApplySelectionVisuals();
|
|
||||||
ApplySegmentSelectionVisuals();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EnsureStyleSheet(VisualElement container)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!EnsureUIDocument()) return;
|
|
||||||
var root = uiDocument!.rootVisualElement;
|
|
||||||
if (defaultStyleSheet != null)
|
|
||||||
{
|
|
||||||
if (!root.styleSheets.Contains(defaultStyleSheet)) root.styleSheets.Add(defaultStyleSheet);
|
|
||||||
if (!container.styleSheets.Contains(defaultStyleSheet)) container.styleSheets.Add(defaultStyleSheet);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Resources/Styles/GanttChart.uss 를 시도
|
|
||||||
var ss = Resources.Load<StyleSheet>("SHI/Styles/GanttChart");
|
|
||||||
if (ss != null)
|
|
||||||
{
|
|
||||||
if (!root.styleSheets.Contains(ss)) root.styleSheets.Add(ss);
|
|
||||||
if (!container.styleSheets.Contains(ss)) container.styleSheets.Add(ss);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex) { Debug.LogWarning($"EnsureStyleSheet failed: {ex.Message}"); }
|
|
||||||
}
|
|
||||||
|
|
||||||
private void BuildHeaderRows(VisualElement header, DateTime min, DateTime max, int totalDays, bool addToday)
|
|
||||||
{
|
|
||||||
var timelineWidth = totalDays * dayWidth;
|
|
||||||
|
|
||||||
// Month row
|
|
||||||
var monthRow = new VisualElement { name = "month-row" };
|
|
||||||
monthRow.AddToClassList("month-row");
|
|
||||||
monthRow.style.flexDirection = FlexDirection.Row;
|
|
||||||
monthRow.style.height = 20;
|
|
||||||
header.Add(monthRow);
|
|
||||||
|
|
||||||
// iterate months
|
|
||||||
var cur = new DateTime(min.Year, min.Month, 1);
|
|
||||||
var end = new DateTime(max.Year, max.Month, 1).AddMonths(1).AddDays(-1);
|
|
||||||
if (end < max) end = max;
|
|
||||||
while (cur <= max)
|
|
||||||
{
|
|
||||||
var monthStart = cur < min ? min : cur;
|
|
||||||
var lastDayOfMonth = new DateTime(cur.Year, cur.Month, DateTime.DaysInMonth(cur.Year, cur.Month));
|
|
||||||
var monthEnd = lastDayOfMonth > max ? max : lastDayOfMonth;
|
|
||||||
var days = Math.Max(1, (int)(monthEnd.Date - monthStart.Date).TotalDays + 1);
|
|
||||||
var cell = new Label(cur.ToString("yyyy MMM", CultureInfo.InvariantCulture));
|
|
||||||
cell.AddToClassList("span-cell");
|
|
||||||
cell.style.width = days * dayWidth;
|
|
||||||
cell.style.unityTextAlign = TextAnchor.MiddleCenter;
|
|
||||||
cell.style.color = new Color(0.9f, 0.9f, 0.9f, 1f); // 밝은 헤더 텍스트
|
|
||||||
monthRow.Add(cell);
|
|
||||||
cur = cur.AddMonths(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Week row (ISO Week, Mon-Sun)
|
|
||||||
var weekRow = new VisualElement { name = "week-row" };
|
|
||||||
weekRow.AddToClassList("week-row");
|
|
||||||
weekRow.style.flexDirection = FlexDirection.Row;
|
|
||||||
weekRow.style.height = 20;
|
|
||||||
header.Add(weekRow);
|
|
||||||
|
|
||||||
var culture = CultureInfo.InvariantCulture;
|
|
||||||
var cal = culture.Calendar;
|
|
||||||
var weekRule = CalendarWeekRule.FirstFourDayWeek;
|
|
||||||
var firstDay = DayOfWeek.Monday;
|
|
||||||
var d = min.Date;
|
|
||||||
while (d <= max.Date)
|
|
||||||
{
|
|
||||||
var isoWeek = cal.GetWeekOfYear(d, weekRule, firstDay);
|
|
||||||
// span until week end or max
|
|
||||||
var weekEnd = d.AddDays((7 + (int)DayOfWeek.Sunday - (int)d.DayOfWeek) % 7); // upcoming Sunday (inclusive)
|
|
||||||
if (weekEnd > max.Date) weekEnd = max.Date;
|
|
||||||
var days = Math.Max(1, (int)(weekEnd - d).TotalDays + 1);
|
|
||||||
var cell = new Label($"W{isoWeek}");
|
|
||||||
cell.AddToClassList("span-cell");
|
|
||||||
cell.style.width = days * dayWidth;
|
|
||||||
cell.style.unityTextAlign = TextAnchor.MiddleCenter;
|
|
||||||
cell.style.color = new Color(0.85f, 0.85f, 0.85f, 1f);
|
|
||||||
weekRow.Add(cell);
|
|
||||||
d = weekEnd.AddDays(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Day row
|
|
||||||
var dayRow = new VisualElement { name = "day-row" };
|
|
||||||
dayRow.AddToClassList("day-row");
|
|
||||||
dayRow.style.flexDirection = FlexDirection.Row;
|
|
||||||
dayRow.style.height = 20;
|
|
||||||
dayRow.style.width = timelineWidth;
|
|
||||||
var dc = min.Date;
|
|
||||||
while (dc <= max.Date)
|
|
||||||
{
|
|
||||||
var cell = new Label(dc.Day.ToString()) { name = $"day-{dc:yyyyMMdd}" };
|
|
||||||
cell.AddToClassList("day-cell");
|
|
||||||
cell.style.width = dayWidth;
|
|
||||||
cell.style.unityTextAlign = TextAnchor.MiddleCenter;
|
|
||||||
cell.style.color = new Color(0.8f, 0.8f, 0.8f, 1f);
|
|
||||||
dayRow.Add(cell);
|
|
||||||
dc = dc.AddDays(1);
|
|
||||||
}
|
|
||||||
header.Add(dayRow);
|
|
||||||
|
|
||||||
// Today line on header
|
|
||||||
if (addToday)
|
|
||||||
{
|
|
||||||
var today = DateTime.Today.Date;
|
|
||||||
if (today >= min.Date && today <= max.Date)
|
|
||||||
{
|
|
||||||
var leftDays = (int)(today - min.Date).TotalDays;
|
|
||||||
var line = new VisualElement { name = "today-line-header" };
|
|
||||||
line.AddToClassList("today-line");
|
|
||||||
line.style.position = Position.Absolute;
|
|
||||||
line.style.left = leftDays * dayWidth;
|
|
||||||
line.style.width = 1;
|
|
||||||
line.style.top = 0;
|
|
||||||
line.style.bottom = 0;
|
|
||||||
line.style.backgroundColor = new Color(1f, 0.42f, 0f, 1f);
|
|
||||||
header.Add(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_monthRowRef = monthRow; _weekRowRef = weekRow; _dayRowRef = dayRow;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HookHeaderScrollSync()
|
|
||||||
{
|
|
||||||
if (_bodyScrollRef == null) return;
|
|
||||||
void Sync()
|
|
||||||
{ try { var off = _bodyScrollRef.scrollOffset; float x = off.x; if (_monthRowRef != null) _monthRowRef.style.marginLeft = -x; if (_weekRowRef != null) _weekRowRef.style.marginLeft = -x; if (_dayRowRef != null) _dayRowRef.style.marginLeft = -x; } catch { } }
|
|
||||||
// 초기1회
|
|
||||||
Sync();
|
|
||||||
try { if (_bodyScrollRef.horizontalScroller != null) _bodyScrollRef.horizontalScroller.valueChanged += _ => Sync(); } catch { }
|
|
||||||
// fallback: schedule polling (lightweight)
|
|
||||||
_bodyScrollRef.schedule.Execute(() => Sync()).Every(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void BuildRow(VisualElement container, string key, DateTime min, float timelineWidth, BlockScheduleRow row)
|
|
||||||
{
|
|
||||||
var rowVE = new VisualElement { name = "row-" + key };
|
|
||||||
rowVE.AddToClassList("chart-row"); rowVE.style.flexDirection = FlexDirection.Row; rowVE.style.height = rowHeight; rowVE.style.borderBottomWidth = 1; rowVE.style.borderBottomColor = new Color(0, 0, 0, 0.4f);
|
|
||||||
|
|
||||||
var label = new Label(string.IsNullOrEmpty(row.BlockNo) ? (row.L1 ?? key) : row.BlockNo) { name = "row-label" };
|
|
||||||
label.AddToClassList("hierarchy-cell"); label.style.width = 240; rowVE.Add(label);
|
|
||||||
|
|
||||||
// Segment layer
|
|
||||||
var segLayer = new VisualElement { name = "segments" };
|
|
||||||
segLayer.AddToClassList("segments-layer"); segLayer.style.flexGrow = 1; segLayer.style.position = Position.Relative; segLayer.style.width = timelineWidth; rowVE.Add(segLayer);
|
|
||||||
|
|
||||||
bool anyRendered = false;
|
|
||||||
foreach (var kv in row.Periods)
|
|
||||||
{
|
|
||||||
var code = kv.Key; var dr = kv.Value;
|
|
||||||
if (_filterCode != null && !string.Equals(code, _filterCode, StringComparison.OrdinalIgnoreCase)) continue;
|
|
||||||
if (_filterFrom.HasValue || _filterTo.HasValue)
|
|
||||||
{ var start = dr.Start; var end = dr.End ?? dr.Start; if (start.HasValue) { var s = start.Value.Date; var e = end.HasValue ? end.Value.Date : s; if (!IsOverlapping(_filterFrom, _filterTo, s, e)) continue; } }
|
|
||||||
if (!dr.Start.HasValue) continue;
|
|
||||||
if (dr.End.HasValue && dr.End.Value.Date < dr.Start.Value.Date) { Debug.LogWarning($"Start > End detected. Skip: key={key} code={code} start={dr.Start:yyyyMMdd} end={dr.End:yyyyMMdd}"); continue; }
|
|
||||||
var segKey = (row.BlockNo ?? key) + "|" + code;
|
|
||||||
if (dr.Start.HasValue && dr.End.HasValue)
|
|
||||||
{
|
|
||||||
var leftDays = (int)(dr.Start.Value.Date - min.Date).TotalDays; var durDays = Math.Max(1, (int)(dr.End.Value.Date - dr.Start.Value.Date).TotalDays + 1);
|
|
||||||
var seg = new VisualElement { name = $"seg-{code}" };
|
|
||||||
seg.AddToClassList("segment"); seg.AddToClassList($"seg-code-{code}");
|
|
||||||
seg.style.position = Position.Absolute; seg.style.left = leftDays * dayWidth; seg.style.width = durDays * dayWidth; seg.style.height = rowHeight - 4; seg.style.backgroundColor = new Color(0.25f, 0.55f, 0.85f, 0.6f);
|
|
||||||
seg.style.borderTopLeftRadius = 3; seg.style.borderBottomLeftRadius = 3; seg.style.borderTopRightRadius = 3; seg.style.borderBottomRightRadius = 3;
|
|
||||||
seg.tooltip = $"{row.BlockNo ?? key}|{code}: {dr.Start:yyyy-MM-dd} ~ {dr.End:yyyy-MM-dd}";
|
|
||||||
seg.RegisterCallback<ClickEvent>(_ => { _selectedKey = row.BlockNo ?? key; _selectedSegmentKey = segKey; ApplySelectionVisuals(); ApplySegmentSelectionVisuals(); OnRowClickedByKey?.Invoke(row.BlockNo ?? key); });
|
|
||||||
segLayer.Add(seg); if (!_segmentElements.ContainsKey(segKey)) _segmentElements[segKey] = seg; anyRendered = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var leftDays = (int)(dr.Start.Value.Date - min.Date).TotalDays;
|
|
||||||
var marker = new Label("@") { name = $"mark-{code}" };
|
|
||||||
marker.AddToClassList("marker"); marker.AddToClassList($"marker-code-{code}");
|
|
||||||
marker.style.position = Position.Absolute; marker.style.left = leftDays * dayWidth; marker.style.width = dayWidth; marker.style.height = rowHeight - 4; marker.style.unityTextAlign = TextAnchor.MiddleCenter; marker.style.color = new Color(0.2f, 0.8f, 1f, 1f);
|
|
||||||
marker.tooltip = $"{row.BlockNo ?? key}|{code}: {dr.Start:yyyy-MM-dd}";
|
|
||||||
marker.RegisterCallback<ClickEvent>(_ => { _selectedKey = row.BlockNo ?? key; _selectedSegmentKey = segKey; ApplySelectionVisuals(); ApplySegmentSelectionVisuals(); OnRowClickedByKey?.Invoke(row.BlockNo ?? key); });
|
|
||||||
segLayer.Add(marker); if (!_segmentElements.ContainsKey(segKey)) _segmentElements[segKey] = marker; anyRendered = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!anyRendered) return; // 필터에 의해 비어지면 행 숨김
|
|
||||||
container.Add(rowVE);
|
|
||||||
if (!string.IsNullOrEmpty(key) && !_rowElements.ContainsKey(key)) _rowElements[key] = rowVE;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsOverlapping(DateTime? from, DateTime? to, DateTime s, DateTime e)
|
|
||||||
{ if (!from.HasValue && !to.HasValue) return true; if (from.HasValue && e < from.Value.Date) return false; if (to.HasValue && s > to.Value.Date) return false; return true; }
|
|
||||||
|
|
||||||
private void ApplySelectionVisuals()
|
|
||||||
{
|
|
||||||
// 모든 하이라이트 제거
|
|
||||||
foreach (var kv in _rowElements)
|
|
||||||
{
|
|
||||||
kv.Value.RemoveFromClassList("row-selected");
|
|
||||||
// 백업 인라인 스타일 제거/초기화
|
|
||||||
kv.Value.style.backgroundColor = StyleKeyword.Null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(_selectedKey)) return;
|
|
||||||
if (_rowElements.TryGetValue(_selectedKey, out var ve))
|
|
||||||
{
|
|
||||||
ve.AddToClassList("row-selected");
|
|
||||||
// USS 없을 때 가시성 확보용 백업 스타일
|
|
||||||
ve.style.backgroundColor = new Color(1f, 1f, 0f, 0.15f);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Debug.Log($"ApplySelectionVisuals: row not found for key={_selectedKey}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ApplySegmentSelectionVisuals()
|
|
||||||
{
|
|
||||||
foreach (var kv in _segmentElements) kv.Value.RemoveFromClassList("segment-selected");
|
|
||||||
if (string.IsNullOrEmpty(_selectedSegmentKey)) return;
|
|
||||||
if (_segmentElements.TryGetValue(_selectedSegmentKey, out var ve)) ve.AddToClassList("segment-selected");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static GanttChartData CreateDummyData()
|
|
||||||
{
|
|
||||||
var min = DateTime.Today.AddDays(-3); var max = DateTime.Today.AddDays(10);
|
|
||||||
var row1 = new BlockScheduleRow { BlockNo = "DUMMY-1" }; row1.Periods["21"] = new DateRange { Start = min.AddDays(2), End = min.AddDays(6) }; row1.Periods["23"] = new DateRange { Start = min.AddDays(8) };
|
|
||||||
var row2 = new BlockScheduleRow { BlockNo = "DUMMY-2" }; row2.Periods["21"] = new DateRange { Start = min.AddDays(1), End = min.AddDays(3) }; row2.Periods["23"] = new DateRange { Start = min.AddDays(5), End = max };
|
|
||||||
return new GanttChartData { Rows = new List<BlockScheduleRow> { row1, row2 }, MinDate = min, MaxDate = max };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to ensure a UIDocument exists so the view can render even if not wired in inspector
|
|
||||||
private bool EnsureUIDocument()
|
|
||||||
{
|
|
||||||
if (uiDocument != null) return true;
|
|
||||||
|
|
||||||
// Try get existing on this GameObject
|
|
||||||
var ud = GetComponent<UIDocument>();
|
|
||||||
if (ud == null)
|
|
||||||
{
|
|
||||||
// Create a new UIDocument component
|
|
||||||
try { ud = gameObject.AddComponent<UIDocument>(); }
|
|
||||||
catch (Exception ex) { Debug.LogWarning($"Failed to add UIDocument: {ex.Message}"); return false; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try find or create PanelSettings
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (ud.panelSettings == null)
|
|
||||||
{
|
|
||||||
// Try Resources first
|
|
||||||
var ps = Resources.Load<PanelSettings>("SHI/PanelSettings/DefaultPanelSettings");
|
|
||||||
if (ps == null)
|
|
||||||
{
|
|
||||||
// Create runtime instance as fallback (not saved as asset)
|
|
||||||
ps = ScriptableObject.CreateInstance<PanelSettings>();
|
|
||||||
}
|
|
||||||
ud.panelSettings = ps;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
uiDocument = ud;
|
|
||||||
return uiDocument != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep UI Toolkit container sized to this RectTransform
|
|
||||||
private void SyncContainerSize()
|
|
||||||
{
|
|
||||||
if (!EnsureUIDocument()) return;
|
|
||||||
var rt = GetComponent<RectTransform>();
|
|
||||||
if (rt == null) return;
|
|
||||||
var size = rt.rect.size;
|
|
||||||
var root = uiDocument!.rootVisualElement;
|
|
||||||
var container = root.Q(chartRootName) ?? new VisualElement { name = chartRootName };
|
|
||||||
if (container.parent == null) root.Add(container);
|
|
||||||
container.style.position = Position.Absolute;
|
|
||||||
container.style.width = size.x;
|
|
||||||
container.style.height = size.y;
|
|
||||||
|
|
||||||
// Adjust body scroll height if available (header fixed height60)
|
|
||||||
var body = container.Q("body-scroll");
|
|
||||||
if (body != null)
|
|
||||||
{
|
|
||||||
var h = Mathf.Max(0, size.y - 60f);
|
|
||||||
body.style.height = h;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SyncContainerPosition()
|
|
||||||
{
|
|
||||||
if (!EnsureUIDocument()) return;
|
|
||||||
var rt = GetComponent<RectTransform>(); if (rt == null) return;
|
|
||||||
var canvas = GetComponentInParent<Canvas>();
|
|
||||||
Camera? cam = null;
|
|
||||||
if (canvas != null)
|
|
||||||
{
|
|
||||||
if (canvas.renderMode == RenderMode.ScreenSpaceCamera || canvas.renderMode == RenderMode.WorldSpace)
|
|
||||||
cam = canvas.worldCamera != null ? canvas.worldCamera : Camera.main;
|
|
||||||
}
|
|
||||||
var corners = new Vector3[4];
|
|
||||||
rt.GetWorldCorners(corners); //0:BL,1:TL,2:TR,3:BR
|
|
||||||
var tlWorld = corners[1];
|
|
||||||
var tlScreen = RectTransformUtility.WorldToScreenPoint(cam, tlWorld);
|
|
||||||
var left = tlScreen.x;
|
|
||||||
var top = Screen.height - tlScreen.y; // convert to top-left origin
|
|
||||||
var root = uiDocument!.rootVisualElement;
|
|
||||||
var container = root.Q(chartRootName) ?? new VisualElement { name = chartRootName };
|
|
||||||
if (container.parent == null) root.Add(container);
|
|
||||||
container.style.position = Position.Absolute;
|
|
||||||
container.style.left = left;
|
|
||||||
container.style.top = top;
|
|
||||||
|
|
||||||
if (logAlignmentDebug)
|
|
||||||
Debug.Log($"Align container to RectTransform TL screen=({left:F1},{top:F1}) size=({rt.rect.width:F1},{rt.rect.height:F1})");
|
|
||||||
}
|
|
||||||
|
|
||||||
// React to RectTransform size changes
|
|
||||||
private void OnRectTransformDimensionsChange()
|
|
||||||
{
|
|
||||||
SyncContainerSize();
|
|
||||||
SyncContainerPosition();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 78e6249d32aa43149ab7f5e12a71967c
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
#nullable enable
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.UIElements;
|
|
||||||
|
|
||||||
namespace SHI.modal
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 간트 데이터 로드 및(임시) UI Toolkit 렌더 스텁.
|
|
||||||
/// </summary>
|
|
||||||
public class ModelDetailISOPChartView : MonoBehaviour
|
|
||||||
{
|
|
||||||
public Action<string>? OnRowClickedByKey;
|
|
||||||
public Action<Guid>? OnRowClicked;
|
|
||||||
|
|
||||||
[SerializeField] private ShipblockGanttController? ganttController;
|
|
||||||
|
|
||||||
|
|
||||||
public void Start()
|
|
||||||
{
|
|
||||||
LoadFromStreamingAssets();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void LoadFromStreamingAssets(string fileName = "isop_chart.json")
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
|
|
||||||
var path = Path.Combine(Application.streamingAssetsPath, fileName);
|
|
||||||
if (!File.Exists(path))
|
|
||||||
{
|
|
||||||
Debug.LogError($"File not found: {path}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
ganttController?.Load(path);
|
|
||||||
Debug.LogWarning("Unsupported chart JSON format.");
|
|
||||||
}
|
|
||||||
catch (Exception ex) { Debug.LogError($"LoadFromStreamingAssets failed: {ex.Message}"); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 1064f370279bf304dac7b13c8b4e6577
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
#nullable enable
|
|
||||||
using UnityEngine;
|
|
||||||
using UVC.UI.Buttons;
|
|
||||||
using UVC.UI.List.Tree;
|
|
||||||
|
|
||||||
namespace SHI.modal
|
|
||||||
{
|
|
||||||
public class ModelDetailListItem : TreeListItem
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 가시성 상태를 표시하는 배경 이미지.
|
|
||||||
/// </summary>
|
|
||||||
[SerializeField]
|
|
||||||
protected ImageToggle visibleToggle;
|
|
||||||
|
|
||||||
public override void Init(TreeListItemData data, TreeList control, TreeListDragDropManager dragDropManager)
|
|
||||||
{
|
|
||||||
base.Init(data, control, dragDropManager);
|
|
||||||
if (visibleToggle != null)
|
|
||||||
{
|
|
||||||
if (data is ModelDetailListItemData modelData) {
|
|
||||||
visibleToggle.isOn = modelData.IsVisible;
|
|
||||||
}
|
|
||||||
|
|
||||||
visibleToggle.OnValueChanged.AddListener(isOn =>
|
|
||||||
{
|
|
||||||
if (data is ModelDetailListItemData modelData)
|
|
||||||
{
|
|
||||||
modelData.IsVisible = isOn;
|
|
||||||
modelData.OnClickVisibleAction?.Invoke(modelData, isOn);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnDataChanged(ChangedType changedType, TreeListItemData changedData, int index)
|
|
||||||
{
|
|
||||||
if (changedType == ChangedType.TailButtons && changedData is ModelDetailListItemData modelData)
|
|
||||||
{
|
|
||||||
if (visibleToggle != null)
|
|
||||||
{
|
|
||||||
visibleToggle.isOn = modelData.IsVisible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
base.OnDataChanged(changedType, changedData, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnDestroy()
|
|
||||||
{
|
|
||||||
if (visibleToggle != null) visibleToggle.OnValueChanged.RemoveAllListeners();
|
|
||||||
base.OnDestroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 1e59b82317d5c194ab2ab4e87730e631
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
#nullable enable
|
|
||||||
using System;
|
|
||||||
using UVC.UI.List.Tree;
|
|
||||||
|
|
||||||
namespace SHI.modal
|
|
||||||
{
|
|
||||||
public class ModelDetailListItemData : TreeListItemData
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 아이템의 가시성 아이콘 클릭 시 실행할 사용자 정의 동작.
|
|
||||||
/// </summary>
|
|
||||||
public Action<TreeListItemData, bool>? OnClickVisibleAction;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 리스트/모델 가시성 상태
|
|
||||||
/// </summary>
|
|
||||||
public bool IsVisible { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 외부(간트/백엔드)와의 안정 매핑용 키. 예: GLTF 노드의 풀 경로("/Root/Level1/Beam023").
|
|
||||||
/// </summary>
|
|
||||||
public string ExternalKey { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
|
|
||||||
public override TreeListItemData Clone()
|
|
||||||
{
|
|
||||||
ModelDetailListItemData clone = new ModelDetailListItemData();
|
|
||||||
clone._id = this.Id;
|
|
||||||
clone.Name = this.Name;
|
|
||||||
clone.Option = this.Option;
|
|
||||||
clone.IsExpanded = this.IsExpanded;
|
|
||||||
clone.IsSelected = this.IsSelected;
|
|
||||||
clone.OnClickAction = this.OnClickAction;
|
|
||||||
clone.OnSelectionChanged = this.OnSelectionChanged;
|
|
||||||
clone.OnDataChanged = this.OnDataChanged;
|
|
||||||
clone.IsVisible = this.IsVisible;
|
|
||||||
clone.ExternalKey = this.ExternalKey;
|
|
||||||
clone.OnClickVisibleAction = this.OnClickVisibleAction;
|
|
||||||
return clone;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 21fbea5ce75b98f489f259fc7cf88fe3
|
|
||||||
@@ -1,536 +0,0 @@
|
|||||||
#nullable enable
|
|
||||||
using Cysharp.Threading.Tasks;
|
|
||||||
using DG.Tweening;
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using TMPro;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.UI;
|
|
||||||
using UVC.UI.List.Tree;
|
|
||||||
|
|
||||||
namespace SHI.modal
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 계층 데이터를 표시/검색/선택하는 창(View)입니다.
|
|
||||||
///
|
|
||||||
/// 책임:
|
|
||||||
/// - 메인 트리(`treeList`)와 검색 트리(`treeListSearch`)를 관리
|
|
||||||
/// - 입력창으로 검색을 수행하고 결과를 검색 트리에 표시(청크 처리 + 로딩 애니메이션)
|
|
||||||
/// - `TreeList.OnItemSelectionChanged`를 구독해 외부로 선택/해제 이벤트를 전달
|
|
||||||
/// - 외부에서 호출 가능한 간단한 항목 추가/삭제 API 제공(실제 렌더링/상태는 `TreeList`가 담당)
|
|
||||||
///
|
|
||||||
/// 사용 예:
|
|
||||||
/// <example>
|
|
||||||
/// <![CDATA[
|
|
||||||
/// // 외부에서 창을 참조했다고 가정
|
|
||||||
/// accordionWindow.OnItemSelected += item => Debug.Log($"Selected: {item.Name}");
|
|
||||||
/// accordionWindow.AddItem(new TreeListItemData("Root A"));
|
|
||||||
/// accordionWindow.AddItemAt(new TreeListItemData("Root B"),0);
|
|
||||||
/// ]]>
|
|
||||||
/// </example>
|
|
||||||
/// </summary>
|
|
||||||
public class ModelDetailListView : MonoBehaviour
|
|
||||||
{
|
|
||||||
[SerializeField]
|
|
||||||
protected TreeList treeList;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 검색 결과 용 목록
|
|
||||||
/// </summary>
|
|
||||||
[SerializeField]
|
|
||||||
protected TreeList treeListSearch;
|
|
||||||
|
|
||||||
[SerializeField]
|
|
||||||
protected TMP_InputField inputField;
|
|
||||||
|
|
||||||
[SerializeField]
|
|
||||||
protected Button clearTextButton;
|
|
||||||
|
|
||||||
[SerializeField]
|
|
||||||
protected Image loadingImage;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 메인/검색 리스트에서 항목이 선택될 때 발생합니다.
|
|
||||||
/// </summary>
|
|
||||||
public Action<TreeListItemData>? OnItemSelected;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 메인/검색 리스트에서 항목이 선택 해제될 때 발생합니다.
|
|
||||||
/// </summary>
|
|
||||||
public Action<TreeListItemData>? OnItemDeselected;
|
|
||||||
|
|
||||||
public Action? OnClosed;
|
|
||||||
|
|
||||||
// NEW: 가시성 변경 이벤트(Guid, bool)
|
|
||||||
public Action<Guid, bool>? OnVisibilityChanged;
|
|
||||||
|
|
||||||
// 검색 목록에서 선택된 항목(클론된 데이터)
|
|
||||||
protected TreeListItemData? selectedSearchItem;
|
|
||||||
|
|
||||||
|
|
||||||
// 검색 작업 상태
|
|
||||||
protected CancellationTokenSource? searchCts;
|
|
||||||
protected bool isSearching = false;
|
|
||||||
protected float searchProgress = 0f; // 내부 진행도 추적용
|
|
||||||
|
|
||||||
[SerializeField]
|
|
||||||
[Tooltip("로딩 아이콘 회전 속도(도/초)")]
|
|
||||||
protected float loadingRotateSpeed = 360f;
|
|
||||||
|
|
||||||
[SerializeField]
|
|
||||||
[Tooltip("로딩 이미지의 채우기 애니메이션 속도(사이클/초)")]
|
|
||||||
protected float loadingFillCycle = 0.5f; // cycles per second. loadingRotateSpeed360 일때, loadingFillCycle를0.5 보다 높게 설정하면 이상해 보임
|
|
||||||
|
|
||||||
// DOTween tweens
|
|
||||||
protected Tween? loadingRotationTween;
|
|
||||||
protected Tween? loadingFillTween;
|
|
||||||
|
|
||||||
|
|
||||||
protected void Awake()
|
|
||||||
{
|
|
||||||
loadingImage.gameObject.SetActive(false);
|
|
||||||
|
|
||||||
treeListSearch.gameObject.SetActive(false);
|
|
||||||
inputField.onSubmit.AddListener(OnInputFieldSubmit);
|
|
||||||
|
|
||||||
// 메인 리스트 선택 변경을 외부 이벤트로 전달
|
|
||||||
if (treeList != null)
|
|
||||||
{
|
|
||||||
treeList.OnItemSelectionChanged += HandleMainSelectionChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 검색 리스트의 선택 변경을 감지 (선택 결과를 원본 트리에 반영하는 용도)
|
|
||||||
if (treeListSearch != null)
|
|
||||||
{
|
|
||||||
treeListSearch.OnItemSelectionChanged += OnSearchSelectionChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearTextButton.onClick.AddListener(() =>
|
|
||||||
{
|
|
||||||
// 취소
|
|
||||||
CancelSearch();
|
|
||||||
|
|
||||||
// 검색에서 선택한 항목이 있으면 원본 트리에서 동일 항목을 선택하고 펼침
|
|
||||||
if (selectedSearchItem != null && treeList != null)
|
|
||||||
{
|
|
||||||
// 원본 데이터 찾기 (TreeListItemData == 연산자는 Id 기반)
|
|
||||||
var target = treeList.AllItemDataFlattened.FirstOrDefault(i => i == selectedSearchItem);
|
|
||||||
if (target != null)
|
|
||||||
{
|
|
||||||
ClearSelection();
|
|
||||||
|
|
||||||
// 부모 체인을 펼치고 선택 처리
|
|
||||||
treeList.RevealAndSelectItem(target, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedSearchItem = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// NEW: 데이터 주입
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="items"></param>
|
|
||||||
public void SetupData(IEnumerable<ModelDetailListItemData> items)
|
|
||||||
{
|
|
||||||
// 기존 아이템 클리어
|
|
||||||
treeList.ClearItems();
|
|
||||||
SetVisiblilityAction(items);
|
|
||||||
AddItems(items);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetVisiblilityAction(IEnumerable<ModelDetailListItemData> items)
|
|
||||||
{
|
|
||||||
foreach (var item in items)
|
|
||||||
{
|
|
||||||
// 가시성 아이콘 클릭 연동
|
|
||||||
item.OnClickVisibleAction = (data, isVisible) =>
|
|
||||||
{
|
|
||||||
var md = data as ModelDetailListItemData;
|
|
||||||
if (md != null)
|
|
||||||
{
|
|
||||||
md.IsVisible = isVisible;
|
|
||||||
OnVisibilityChanged?.Invoke(md.Id, isVisible);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if(item.Children != null && item.Children.Count > 0)
|
|
||||||
{
|
|
||||||
SetVisiblilityAction(item.Children.OfType<ModelDetailListItemData>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Guid 기반 선택
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id"></param>
|
|
||||||
public void SelectByItemId(Guid id)
|
|
||||||
{
|
|
||||||
var target = treeList.AllItemDataFlattened.FirstOrDefault(t => t.Id == id);
|
|
||||||
if (target != null)
|
|
||||||
{
|
|
||||||
ClearSelection();
|
|
||||||
treeList.RevealAndSelectItem(target, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 메인 트리에 항목을 추가합니다.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">추가할 데이터.</param>
|
|
||||||
public void AddItem(TreeListItemData data)
|
|
||||||
{
|
|
||||||
treeList.AddItem<ModelDetailListItem>(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 메인 트리에 항목들을 추가합니다.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dataList">추가 할 데이터들</param>
|
|
||||||
public void AddItems(IEnumerable<TreeListItemData> dataList)
|
|
||||||
{
|
|
||||||
treeList.AddItems<ModelDetailListItem>(dataList);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 메인 트리에 항목을 특정 인덱스에 삽입합니다.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">삽입할 데이터.</param>
|
|
||||||
/// <param name="index">삽입 인덱스(0 기반).</param>
|
|
||||||
public void AddItemAt(TreeListItemData data, int index)
|
|
||||||
{
|
|
||||||
treeList.AddItemAt<ModelDetailListItem>(data, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 메인 트리에서 항목을 제거합니다(뷰만 제거, 데이터는 호출 측 정책에 따름).
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">제거할 데이터.</param>
|
|
||||||
public void RemoveItem(TreeListItemData data)
|
|
||||||
{
|
|
||||||
treeList.RemoveItem(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 메인 트리에서 항목을 완전히 삭제합니다(뷰+데이터 정리).
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">삭제할 데이터.</param>
|
|
||||||
public void DeleteItem(TreeListItemData data)
|
|
||||||
{
|
|
||||||
treeList.DeleteItem(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 이름으로 아이템 선택
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="name"></param>
|
|
||||||
public void SelectItem(string name)
|
|
||||||
{
|
|
||||||
//검색 중이면 취소
|
|
||||||
CancelSearch();
|
|
||||||
|
|
||||||
treeList.SelectItem(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 이름으로 아이템 선택 해제
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="name"></param>
|
|
||||||
public void DeselectItem(string name)
|
|
||||||
{
|
|
||||||
treeList.DeselectItem(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 선택해제 및 검색 취소
|
|
||||||
/// </summary>
|
|
||||||
public void Clear()
|
|
||||||
{
|
|
||||||
ClearSelection();
|
|
||||||
CancelSearch();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 선택 해제
|
|
||||||
/// </summary>
|
|
||||||
public void ClearSelection()
|
|
||||||
{
|
|
||||||
treeListSearch.ClearSelection();
|
|
||||||
treeList.ClearSelection();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void StartLoadingAnimation()
|
|
||||||
{
|
|
||||||
if (loadingImage == null) return;
|
|
||||||
|
|
||||||
// 기존 트윈 정리
|
|
||||||
StopLoadingAnimation();
|
|
||||||
|
|
||||||
loadingImage.fillAmount = 0f;
|
|
||||||
loadingImage.transform.localRotation = Quaternion.identity;
|
|
||||||
loadingImage.gameObject.SetActive(true);
|
|
||||||
|
|
||||||
// 회전 트윈
|
|
||||||
float rotDuration = (loadingRotateSpeed != 0f) ? (360f / Mathf.Abs(loadingRotateSpeed)) : 1f;
|
|
||||||
loadingRotationTween = loadingImage.transform
|
|
||||||
.DOLocalRotate(new Vector3(0f, 0f, -360f), rotDuration, RotateMode.LocalAxisAdd)
|
|
||||||
.SetEase(Ease.Linear)
|
|
||||||
.SetLoops(-1, LoopType.Restart);
|
|
||||||
|
|
||||||
// 채우기 트윈
|
|
||||||
float fullDuration = (loadingFillCycle > 0f) ? (1f / loadingFillCycle) : 1f;
|
|
||||||
loadingFillTween = DOTween
|
|
||||||
.To(() => loadingImage.fillAmount, x => loadingImage.fillAmount = x, 1f, fullDuration)
|
|
||||||
.SetEase(Ease.InOutSine)
|
|
||||||
.SetLoops(-1, LoopType.Yoyo);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void StopLoadingAnimation()
|
|
||||||
{
|
|
||||||
if (loadingRotationTween != null)
|
|
||||||
{
|
|
||||||
loadingRotationTween.Kill();
|
|
||||||
loadingRotationTween = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loadingFillTween != null)
|
|
||||||
{
|
|
||||||
loadingFillTween.Kill();
|
|
||||||
loadingFillTween = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loadingImage != null)
|
|
||||||
{
|
|
||||||
loadingImage.gameObject.SetActive(false);
|
|
||||||
loadingImage.transform.localRotation = Quaternion.identity;
|
|
||||||
loadingImage.fillAmount = 0f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected void CancelSearch()
|
|
||||||
{
|
|
||||||
// 검색 중이면
|
|
||||||
if (treeListSearch?.gameObject.activeSelf == true)
|
|
||||||
{
|
|
||||||
//검색 시 데이터 변경 된 내용 원본 트리에 반영
|
|
||||||
treeListSearch.AllItemDataFlattened.ToList().ForEach(searchItem =>
|
|
||||||
{
|
|
||||||
var originalItem = treeList.AllItemDataFlattened.FirstOrDefault(i => i == searchItem);
|
|
||||||
if (originalItem != null)
|
|
||||||
{
|
|
||||||
bool changed = false;
|
|
||||||
//선택 상태 동기화
|
|
||||||
if (originalItem.IsSelected != searchItem.IsSelected)
|
|
||||||
{
|
|
||||||
originalItem.IsSelected = searchItem.IsSelected;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
//가시성 상태 동기화 (ModelDetailListItemData 전용)
|
|
||||||
if (originalItem is ModelDetailListItemData originalModelItem &&
|
|
||||||
searchItem is ModelDetailListItemData searchModelItem &&
|
|
||||||
originalModelItem.IsVisible != searchModelItem.IsVisible)
|
|
||||||
{
|
|
||||||
originalModelItem.IsVisible = searchModelItem.IsVisible;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (changed)
|
|
||||||
{
|
|
||||||
//데이터 변경 알림
|
|
||||||
originalItem.NotifyDataChanged(ChangedType.TailButtons, originalItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
inputField.text = string.Empty;
|
|
||||||
|
|
||||||
treeListSearch?.gameObject.SetActive(false);
|
|
||||||
treeList?.gameObject.SetActive(true);
|
|
||||||
|
|
||||||
if (searchCts != null)
|
|
||||||
{
|
|
||||||
try { searchCts.Cancel(); } catch { }
|
|
||||||
searchCts.Dispose();
|
|
||||||
searchCts = null;
|
|
||||||
}
|
|
||||||
isSearching = false;
|
|
||||||
searchProgress = 0f;
|
|
||||||
|
|
||||||
StopLoadingAnimation();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async void OnSearchSelectionChanged(TreeListItemData data, bool isSelected)
|
|
||||||
{
|
|
||||||
if (isSelected)
|
|
||||||
{
|
|
||||||
selectedSearchItem = data;
|
|
||||||
OnItemSelected?.Invoke(data);
|
|
||||||
}
|
|
||||||
else if (selectedSearchItem == data)
|
|
||||||
{
|
|
||||||
selectedSearchItem = null;
|
|
||||||
OnItemDeselected?.Invoke(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void HandleMainSelectionChanged(TreeListItemData data, bool isSelected)
|
|
||||||
{
|
|
||||||
if (isSelected)
|
|
||||||
{
|
|
||||||
OnItemSelected?.Invoke(data);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
OnItemDeselected?.Invoke(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void OnInputFieldSubmit(string text)
|
|
||||||
{
|
|
||||||
// 검색어가 있으면 검색 결과 목록 표시
|
|
||||||
if (!string.IsNullOrEmpty(text))
|
|
||||||
{
|
|
||||||
treeListSearch.ClearSelection();
|
|
||||||
|
|
||||||
treeListSearch.gameObject.SetActive(true);
|
|
||||||
treeList.gameObject.SetActive(false);
|
|
||||||
|
|
||||||
// 시작 애니메이션
|
|
||||||
StartLoadingAnimation();
|
|
||||||
|
|
||||||
searchCts = new CancellationTokenSource();
|
|
||||||
// 비동기 검색 실행(UITask 스타일: 메인스레드에서 작업을 분할하여 UI가 멈추지 않게 함)
|
|
||||||
_ = PerformSearchAsync(text, searchCts.Token);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// 기존 검색 취소
|
|
||||||
CancelSearch();
|
|
||||||
|
|
||||||
treeListSearch.gameObject.SetActive(false);
|
|
||||||
treeList.gameObject.SetActive(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 검색을 메인 스레드에서 분할 처리하여 UI 업데이트(로딩 애니메이션)가 가능하도록 구현합니다.
|
|
||||||
/// </summary>
|
|
||||||
protected async UniTaskVoid PerformSearchAsync(string text, CancellationToken token)
|
|
||||||
{
|
|
||||||
isSearching = true;
|
|
||||||
searchProgress = 0f;
|
|
||||||
|
|
||||||
var results = new List<TreeListItemData>();
|
|
||||||
|
|
||||||
var sourceList = treeList?.AllItemDataFlattened;
|
|
||||||
if (sourceList == null)
|
|
||||||
{
|
|
||||||
isSearching = false;
|
|
||||||
StopLoadingAnimation();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int total = sourceList.Count;
|
|
||||||
if (total == 0)
|
|
||||||
{
|
|
||||||
isSearching = false;
|
|
||||||
StopLoadingAnimation();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 소문자 비교 준비
|
|
||||||
string lower = text.ToLowerInvariant();
|
|
||||||
|
|
||||||
// 분할 처리: 일정 갯수마다 await으로 제어권을 반환
|
|
||||||
const int chunk = 100;
|
|
||||||
for (int i = 0; i < total; i++)
|
|
||||||
{
|
|
||||||
token.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
var item = sourceList[i];
|
|
||||||
if (!string.IsNullOrEmpty(item.Name) && item.Name.ToLowerInvariant().Contains(lower))
|
|
||||||
{
|
|
||||||
results.Add(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 진행도 업데이트 (내부 사용)
|
|
||||||
if ((i % chunk) == 0)
|
|
||||||
{
|
|
||||||
searchProgress = (float)i / (float)total;
|
|
||||||
await UniTask.Yield(PlayerLoopTiming.Update);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 최종 진행도
|
|
||||||
searchProgress = 1f;
|
|
||||||
|
|
||||||
// UI 반영은 메인 스레드에서
|
|
||||||
if (!PlayerLoopHelper.IsMainThread) await UniTask.SwitchToMainThread();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (token.IsCancellationRequested) return;
|
|
||||||
|
|
||||||
treeListSearch.ClearItems();
|
|
||||||
foreach (var r in results)
|
|
||||||
{
|
|
||||||
var cloned = r.Clone();
|
|
||||||
treeListSearch.AddItem<ModelDetailListItem>(cloned);
|
|
||||||
if(cloned.IsSelected)
|
|
||||||
{
|
|
||||||
//선택된 항목은 펼치기
|
|
||||||
treeListSearch.SelectItem(cloned);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 로딩 종료
|
|
||||||
isSearching = false;
|
|
||||||
searchProgress = 0f;
|
|
||||||
StopLoadingAnimation();
|
|
||||||
}
|
|
||||||
catch (System.Exception ex)
|
|
||||||
{
|
|
||||||
Debug.LogError($"PerformSearchAsync error: {ex}");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (searchCts != null)
|
|
||||||
{
|
|
||||||
searchCts.Dispose();
|
|
||||||
searchCts = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Close()
|
|
||||||
{
|
|
||||||
gameObject.SetActive(false);
|
|
||||||
OnClosed?.Invoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected void OnDestroy()
|
|
||||||
{
|
|
||||||
inputField.onSubmit.RemoveListener(OnInputFieldSubmit);
|
|
||||||
clearTextButton.onClick.RemoveAllListeners();
|
|
||||||
|
|
||||||
if (treeListSearch != null)
|
|
||||||
{
|
|
||||||
treeListSearch.OnItemSelectionChanged -= OnSearchSelectionChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (treeList != null)
|
|
||||||
{
|
|
||||||
treeList.OnItemSelectionChanged -= HandleMainSelectionChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
CancelSearch();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 59c744534c4342f4b9d032ed55beb194
|
|
||||||
@@ -1,707 +0,0 @@
|
|||||||
#nullable enable
|
|
||||||
using Cysharp.Threading.Tasks;
|
|
||||||
using GLTFast;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.UI;
|
|
||||||
using UVC.UI.List.Tree;
|
|
||||||
using UVC.Util;
|
|
||||||
|
|
||||||
namespace SHI.modal
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// glTF 모델을 비동기로 로드해 전용 카메라로 오프스크린 렌더링하고, 결과를 <see cref="RawImage"/>에 출력하는 뷰입니다.
|
|
||||||
/// 마우스 조작(이동/확대/회전), 항목 하이라이트, 와이어프레임 토글 등을 제공합니다.
|
|
||||||
/// </summary>
|
|
||||||
public class ModelDetailView : MonoBehaviour
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 뷰 내부에서 항목이 선택될 때 발생합니다.
|
|
||||||
/// </summary>
|
|
||||||
public Action<TreeListItemData>? OnItemSelected;
|
|
||||||
|
|
||||||
[Header("View Output (UI)")]
|
|
||||||
[SerializeField] private RawImage outputImage;
|
|
||||||
[SerializeField] private Color backgroundColor = new Color(0.2f, 0.2f, 0.22f, 1f);
|
|
||||||
[Tooltip("전용 카메라가 렌더링할 레이어. 모델 노드에 재귀적으로 적용됩니다.")]
|
|
||||||
[SerializeField] private int modelLayer = 6;
|
|
||||||
[SerializeField] private bool createDefaultLight = true;
|
|
||||||
|
|
||||||
[Header("Mouse Controls")]
|
|
||||||
[SerializeField] private float panSpeed = 1.0f; // 픽셀→월드 스케일 보정 계수
|
|
||||||
[SerializeField] private float rotateDegPerPixel = 0.2f; // RMB 회전(도/픽셀)
|
|
||||||
[SerializeField] private float zoomSpeed = 5f; // 휠 전/후 모델 이동 속도
|
|
||||||
|
|
||||||
[Header("Output Image Setup")]
|
|
||||||
[SerializeField] private bool autoCreateOutputImage = true;
|
|
||||||
[SerializeField] private bool autoApplyRectMask = true;
|
|
||||||
|
|
||||||
[Header("Wireframe")]
|
|
||||||
[SerializeField] private bool wireframeMode = false; // 켜면 라인만 렌더
|
|
||||||
|
|
||||||
private UnityEngine.Camera _viewCamera;
|
|
||||||
private RenderTexture _rt;
|
|
||||||
private Material _wireframeMat; // Resources에서 로드한 메터리얼
|
|
||||||
private bool _wireframeApplied;
|
|
||||||
|
|
||||||
// Orbit controls state (카메라 위치 계산용)
|
|
||||||
private Vector3 _orbitTarget;
|
|
||||||
private float _orbitDistance = 5f;
|
|
||||||
private float _yaw = 0f;
|
|
||||||
private float _pitch = 20f;
|
|
||||||
|
|
||||||
// Drag state
|
|
||||||
private bool _mmbDragging;
|
|
||||||
private bool _rmbDragging;
|
|
||||||
private Vector3 _mmbLastPos;
|
|
||||||
private Vector3 _rmbStartPos;
|
|
||||||
private float _yawStart;
|
|
||||||
private float _pitchStart;
|
|
||||||
private Quaternion _modelStartRot;
|
|
||||||
private Vector3 _modelStartPos; // RMB 드래그 시작 시 모델 위치
|
|
||||||
private Vector3 _rmbPivot; // RMB 회전의 피벗(모델 중심)
|
|
||||||
|
|
||||||
private readonly Dictionary<Guid, GameObject> _idToObject = new Dictionary<Guid, GameObject>();
|
|
||||||
// 렌더러별 원본 sharedMaterials 캐시(서브트리 중복/충돌 방지)
|
|
||||||
private readonly Dictionary<Renderer, UnityEngine.Material[]> _originalSharedByRenderer = new Dictionary<Renderer, UnityEngine.Material[]>();
|
|
||||||
private GameObject? _root;
|
|
||||||
private Guid? _focusedId;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 주어진 경로의 glTF 모델을 비동기로 로드하고, UI 트리에 사용할 계층 항목을 생성합니다.
|
|
||||||
/// 또한 오프스크린 카메라/RT를 준비하고 모델을 화면에 맞춰 프레이밍합니다.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">glTF/glb 파일 경로(절대/StreamingAssets 기반).</param>
|
|
||||||
/// <param name="ct">취소 토큰.</param>
|
|
||||||
/// <returns>인스턴스화된 노드에 해당하는 항목 계층.</returns>
|
|
||||||
public async UniTask<IEnumerable<ModelDetailListItemData>> LoadModelAsync(string path, CancellationToken ct)
|
|
||||||
{
|
|
||||||
Debug.Log($"ModelDetailView.LoadModelAsync: {path}");
|
|
||||||
Dispose();
|
|
||||||
await UniTask.DelayFrame(1); // Dispose 후1프레임 대기하여 리소스 해제 안정화
|
|
||||||
EnsureCameraAndTargetTexture();
|
|
||||||
|
|
||||||
var items = new List<ModelDetailListItemData>();
|
|
||||||
var gltf = new GltfImport();
|
|
||||||
var success = await gltf.Load(path, new ImportSettings(), ct);
|
|
||||||
if (!success)
|
|
||||||
{
|
|
||||||
Debug.LogError($"glTFast Load failed: {path}");
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
_root = new GameObject("ModelDetailViewRoot");
|
|
||||||
_root.layer = modelLayer;
|
|
||||||
var sceneOk = await gltf.InstantiateMainSceneAsync(_root.transform);
|
|
||||||
if (!sceneOk)
|
|
||||||
{
|
|
||||||
Debug.LogError("InstantiateMainSceneAsync failed");
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
SetLayerRecursive(_root, modelLayer);
|
|
||||||
|
|
||||||
// Build hierarchical item data preserving parent-child relationships
|
|
||||||
if (_root != null)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < _root.transform.childCount; i++)
|
|
||||||
{
|
|
||||||
var child = _root.transform.GetChild(i);
|
|
||||||
var topItem = BuildItemRecursive(child, _root.transform);
|
|
||||||
if (topItem != null)
|
|
||||||
{
|
|
||||||
items.Add(topItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var bounds = CalculateBounds(_root);
|
|
||||||
FrameToBounds(bounds);
|
|
||||||
|
|
||||||
// 요청: Resources/SHI/Shader/BasicWireframe 메터리얼 적용
|
|
||||||
TryLoadWireframeMaterial();
|
|
||||||
if (wireframeMode && _wireframeMat != null)
|
|
||||||
{
|
|
||||||
ApplyWireframeMaterialToRoot();
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ModelDetailListItemData BuildItemRecursive(Transform node, Transform root)
|
|
||||||
{
|
|
||||||
var data = new ModelDetailListItemData
|
|
||||||
{
|
|
||||||
Name = node.name,
|
|
||||||
ExternalKey = BuildFullPath(node, root),
|
|
||||||
IsExpanded = true
|
|
||||||
};
|
|
||||||
|
|
||||||
// map id -> object for selection/highlight and cache materials at this subtree root
|
|
||||||
_idToObject[data.Id] = node.gameObject;
|
|
||||||
CacheOriginalMaterials(node.gameObject);
|
|
||||||
|
|
||||||
// children
|
|
||||||
for (int i = 0; i < node.childCount; i++)
|
|
||||||
{
|
|
||||||
var child = node.GetChild(i);
|
|
||||||
var childData = BuildItemRecursive(child, root);
|
|
||||||
data.AddChild(childData);
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TryLoadWireframeMaterial()
|
|
||||||
{
|
|
||||||
if (_wireframeMat == null)
|
|
||||||
{
|
|
||||||
_wireframeMat = Resources.Load<Material>("SHI/Shader/BasicWireframe");
|
|
||||||
if (_wireframeMat == null)
|
|
||||||
{
|
|
||||||
Debug.LogWarning("BasicWireframe material not found at Resources/SHI/Shader/BasicWireframe. Wireframe will be disabled.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ApplyWireframeMaterialToRoot()
|
|
||||||
{
|
|
||||||
if (_root == null || _wireframeMat == null) return;
|
|
||||||
var rends = _root.GetComponentsInChildren<Renderer>(true);
|
|
||||||
foreach (var r in rends)
|
|
||||||
{
|
|
||||||
if (r == null) continue;
|
|
||||||
var count = Mathf.Max(1, r.sharedMaterials.Length);
|
|
||||||
var arr = new Material[count];
|
|
||||||
for (int i = 0; i < count; i++) arr[i] = _wireframeMat;
|
|
||||||
r.sharedMaterials = arr;
|
|
||||||
}
|
|
||||||
_wireframeApplied = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RestoreAllOriginalMaterials()
|
|
||||||
{
|
|
||||||
foreach (var kv in _originalSharedByRenderer)
|
|
||||||
{
|
|
||||||
var r = kv.Key;
|
|
||||||
if (r == null) continue;
|
|
||||||
var originals = kv.Value;
|
|
||||||
r.sharedMaterials = originals;
|
|
||||||
}
|
|
||||||
_wireframeApplied = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void EnsureCameraAndTargetTexture()
|
|
||||||
{
|
|
||||||
EnsureOutputImageUI();
|
|
||||||
|
|
||||||
if (_viewCamera == null)
|
|
||||||
{
|
|
||||||
// UI와 분리된 월드 공간 리그 생성(레이아웃 영향 차단)
|
|
||||||
var rig = new GameObject("ModelDetailViewRig");
|
|
||||||
rig.layer = modelLayer;
|
|
||||||
rig.transform.SetParent(null, false);
|
|
||||||
|
|
||||||
var camGo = new GameObject("ModelDetailViewCamera");
|
|
||||||
camGo.layer = modelLayer;
|
|
||||||
camGo.transform.SetParent(rig.transform, false);
|
|
||||||
_viewCamera = camGo.AddComponent<UnityEngine.Camera>();
|
|
||||||
_viewCamera.clearFlags = CameraClearFlags.SolidColor;
|
|
||||||
_viewCamera.backgroundColor = backgroundColor;
|
|
||||||
_viewCamera.nearClipPlane = 0.01f;
|
|
||||||
_viewCamera.farClipPlane = 5000f;
|
|
||||||
_viewCamera.cullingMask = (modelLayer >= 0 && modelLayer <= 31) ? (1 << modelLayer) : ~0;
|
|
||||||
// RT 미할당 시 메인 디스플레이 출력 방지
|
|
||||||
_viewCamera.targetDisplay = 1; // secondary display
|
|
||||||
_viewCamera.enabled = false; // RT가 바인딩될 때만 활성화
|
|
||||||
}
|
|
||||||
|
|
||||||
// 기본 조명(옵션, UI와 독립)
|
|
||||||
if (createDefaultLight && _viewCamera.transform.parent != null && _viewCamera.transform.parent.Find("ModelDetailViewLight") == null)
|
|
||||||
{
|
|
||||||
var lightGo = new GameObject("ModelDetailViewLight");
|
|
||||||
lightGo.layer = modelLayer;
|
|
||||||
lightGo.transform.SetParent(_viewCamera.transform.parent, false);
|
|
||||||
lightGo.transform.localPosition = Vector3.zero;
|
|
||||||
lightGo.transform.localRotation = Quaternion.Euler(50f, -30f, 0f);
|
|
||||||
var light = lightGo.AddComponent<Light>();
|
|
||||||
light.type = LightType.Directional;
|
|
||||||
light.intensity = 1.1f;
|
|
||||||
light.shadows = LightShadows.Soft;
|
|
||||||
}
|
|
||||||
|
|
||||||
EnsureRenderTargetSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EnsureOutputImageUI()
|
|
||||||
{
|
|
||||||
var selfRt = GetComponent<RectTransform>();
|
|
||||||
if (autoApplyRectMask && selfRt != null && selfRt.GetComponent<RectMask2D>() == null)
|
|
||||||
{
|
|
||||||
selfRt.gameObject.AddComponent<RectMask2D>();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outputImage == null && autoCreateOutputImage)
|
|
||||||
{
|
|
||||||
var go = new GameObject("OutputImage", typeof(RectTransform), typeof(RawImage));
|
|
||||||
go.transform.SetParent(transform, false);
|
|
||||||
outputImage = go.GetComponent<RawImage>();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outputImage != null)
|
|
||||||
{
|
|
||||||
var rt = outputImage.rectTransform;
|
|
||||||
rt.anchorMin = Vector2.zero;
|
|
||||||
rt.anchorMax = Vector2.one;
|
|
||||||
rt.offsetMin = Vector2.zero;
|
|
||||||
rt.offsetMax = Vector2.zero;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EnsureRenderTargetSize()
|
|
||||||
{
|
|
||||||
if (_viewCamera == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (outputImage == null)
|
|
||||||
{
|
|
||||||
// 출력 타겟이 없으면 메인 디스플레이 출력 방지
|
|
||||||
_viewCamera.enabled = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var rtf = outputImage.rectTransform;
|
|
||||||
int w = Mathf.Max(64, Mathf.RoundToInt(rtf.rect.width));
|
|
||||||
int h = Mathf.Max(64, Mathf.RoundToInt(rtf.rect.height));
|
|
||||||
if (_rt == null || _rt.width != w || _rt.height != h)
|
|
||||||
{
|
|
||||||
// release old
|
|
||||||
if (_rt != null)
|
|
||||||
{
|
|
||||||
if (_viewCamera.targetTexture == _rt) _viewCamera.targetTexture = null;
|
|
||||||
_rt.Release();
|
|
||||||
Destroy(_rt);
|
|
||||||
}
|
|
||||||
// create new
|
|
||||||
_rt = new RenderTexture(w, h, 24, RenderTextureFormat.ARGB32)
|
|
||||||
{
|
|
||||||
name = "ModelDetailViewRT",
|
|
||||||
antiAliasing = 2
|
|
||||||
};
|
|
||||||
_viewCamera.targetTexture = _rt;
|
|
||||||
outputImage.texture = _rt;
|
|
||||||
// RT 바인딩 시에만 카메라 활성화
|
|
||||||
_viewCamera.enabled = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// ensure binding and enable
|
|
||||||
if (_viewCamera.targetTexture != _rt)
|
|
||||||
{
|
|
||||||
_viewCamera.targetTexture = _rt;
|
|
||||||
}
|
|
||||||
if (!_viewCamera.enabled) _viewCamera.enabled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void OnRectTransformDimensionsChange()
|
|
||||||
{
|
|
||||||
// UI 리사이즈에 따라 RT 크기 동기화
|
|
||||||
EnsureRenderTargetSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 와이어프레임 모드를 토글합니다.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="on">true면 와이어프레임 적용, false면 원본 머티리얼 복원.</param>
|
|
||||||
public void SetWireframe(bool on)
|
|
||||||
{
|
|
||||||
wireframeMode = on;
|
|
||||||
TryLoadWireframeMaterial();
|
|
||||||
if (_wireframeMat != null)
|
|
||||||
{
|
|
||||||
if (on) ApplyWireframeMaterialToRoot(); else RestoreAllOriginalMaterials();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Debug.LogWarning("Wireframe material not found. Wireframe toggle ignored.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 지정한 항목을 포커스(하이라이트)합니다.
|
|
||||||
/// </summary>
|
|
||||||
public void FocusItem(TreeListItemData data)
|
|
||||||
{
|
|
||||||
if (data == null) return;
|
|
||||||
FocusItemById(data.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Guid 식별자로 항목을 포커스(하이라이트)합니다.
|
|
||||||
/// </summary>
|
|
||||||
public void FocusItemById(Guid id)
|
|
||||||
{
|
|
||||||
_focusedId = id;
|
|
||||||
if (_idToObject.TryGetValue(id, out var go))
|
|
||||||
{
|
|
||||||
Highlight(go, true);
|
|
||||||
Debug.Log($"ModelDetailView.FocusItemById: {go.name}");
|
|
||||||
_orbitTarget = go.transform.position;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 포커스를 해제하고 하이라이트를 제거합니다.
|
|
||||||
/// </summary>
|
|
||||||
public void UnfocusItem()
|
|
||||||
{
|
|
||||||
if (_focusedId.HasValue && _idToObject.TryGetValue(_focusedId.Value, out var go))
|
|
||||||
{
|
|
||||||
Highlight(go, false);
|
|
||||||
}
|
|
||||||
_focusedId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Export(Guid id)
|
|
||||||
{
|
|
||||||
Debug.Log($"ModelDetailView.Export: id={id}");
|
|
||||||
if (_idToObject.TryGetValue(id, out var go))
|
|
||||||
{
|
|
||||||
Debug.Log($"Exporting object: {go.name}");
|
|
||||||
UVC.GLTF.GLTFExporter.ExportNodeByExplorer(go);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 인스턴스화된 노드의 활성 상태(가시성)를 변경합니다.
|
|
||||||
/// </summary>
|
|
||||||
public void SetVisibility(Guid id, bool on)
|
|
||||||
{
|
|
||||||
Debug.Log($"ModelDetailView.SetVisibility: id={id} on={on}");
|
|
||||||
if (_idToObject.TryGetValue(id, out var go))
|
|
||||||
{
|
|
||||||
go.SetActive(on);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Common color property names across pipelines/shaders
|
|
||||||
private static readonly string[] ColorProps = new[] {
|
|
||||||
"_WireframeColor", "_WireColor", // wireframe shader variants
|
|
||||||
"_BaseColor", "_Color", "_LineColor", "_TintColor"
|
|
||||||
};
|
|
||||||
|
|
||||||
private bool TrySetColor(Material mat, Color c)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < ColorProps.Length; i++)
|
|
||||||
{
|
|
||||||
var prop = ColorProps[i];
|
|
||||||
if (mat != null && mat.HasProperty(prop)) { mat.SetColor(prop, c); return true; }
|
|
||||||
}
|
|
||||||
// fallback to material.color if supported
|
|
||||||
try { mat.color = c; return true; } catch { /* ignore */ }
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryGetColor(Material mat, out Color c)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < ColorProps.Length; i++)
|
|
||||||
{
|
|
||||||
var prop = ColorProps[i];
|
|
||||||
if (mat != null && mat.HasProperty(prop)) { c = mat.GetColor(prop); return true; }
|
|
||||||
}
|
|
||||||
try { c = mat.color; return true; } catch { /* ignore */ }
|
|
||||||
c = ColorUtil.FromHex("#888888"); return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryGetDefaultWireColor(out Color c)
|
|
||||||
{
|
|
||||||
if (_wireframeMat != null && TryGetColor(_wireframeMat, out c)) return true;
|
|
||||||
c = ColorUtil.FromHex("#888888"); return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Highlight(GameObject go, bool on)
|
|
||||||
{
|
|
||||||
// 런타임 인스턴스 머티리얼을 사용하고, 복원 시 렌더러별 캐시된 원본 색을 참고
|
|
||||||
var rends = go.GetComponentsInChildren<Renderer>(true);
|
|
||||||
if (rends == null || rends.Length == 0) return;
|
|
||||||
|
|
||||||
for (int i = 0; i < rends.Length; i++)
|
|
||||||
{
|
|
||||||
var r = rends[i];
|
|
||||||
if (r == null) continue;
|
|
||||||
|
|
||||||
var mats = r.materials; // instanced
|
|
||||||
for (int m = 0; m < mats.Length; m++)
|
|
||||||
{
|
|
||||||
if (on)
|
|
||||||
{
|
|
||||||
TrySetColor(mats[m], ColorUtil.FromHex("#888814"));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
bool matIsWire = mats[m] != null && (mats[m].HasProperty("_WireframeColor") || mats[m].HasProperty("_WireColor"));
|
|
||||||
if (_wireframeApplied || matIsWire)
|
|
||||||
{
|
|
||||||
Color baseWire;
|
|
||||||
if (TryGetDefaultWireColor(out baseWire))
|
|
||||||
TrySetColor(mats[m], baseWire);
|
|
||||||
else
|
|
||||||
TrySetColor(mats[m], ColorUtil.FromHex("#888888"));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// 캐시된 원본 shared material에서 색 복원 시도
|
|
||||||
Color orig;
|
|
||||||
UnityEngine.Material[] originals;
|
|
||||||
if (_originalSharedByRenderer.TryGetValue(r, out originals)
|
|
||||||
&& m < originals.Length && originals[m] != null
|
|
||||||
&& TryGetColor(originals[m], out orig))
|
|
||||||
{
|
|
||||||
TrySetColor(mats[m], orig);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
TrySetColor(mats[m], ColorUtil.FromHex("#888888"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
r.materials = mats;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 외부 리스너에게 선택 이벤트를 전달합니다.
|
|
||||||
/// </summary>
|
|
||||||
public void RaiseSelected(TreeListItemData data)
|
|
||||||
{
|
|
||||||
OnItemSelected?.Invoke(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Update()
|
|
||||||
{
|
|
||||||
if (_viewCamera == null) return;
|
|
||||||
|
|
||||||
// 영역 히트테스트
|
|
||||||
if (outputImage != null)
|
|
||||||
{
|
|
||||||
var cam = outputImage.canvas != null && outputImage.canvas.renderMode != RenderMode.ScreenSpaceOverlay
|
|
||||||
? outputImage.canvas.worldCamera
|
|
||||||
: null;
|
|
||||||
if (!RectTransformUtility.RectangleContainsScreenPoint(outputImage.rectTransform, Input.mousePosition, cam))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var selfRt = GetComponent<RectTransform>();
|
|
||||||
if (selfRt != null)
|
|
||||||
{
|
|
||||||
var parentCanvas = selfRt.GetComponentInParent<Canvas>();
|
|
||||||
UnityEngine.Camera uiCam = null;
|
|
||||||
if (parentCanvas != null && parentCanvas.renderMode != RenderMode.ScreenSpaceOverlay)
|
|
||||||
uiCam = parentCanvas.worldCamera;
|
|
||||||
if (!RectTransformUtility.RectangleContainsScreenPoint(selfRt, Input.mousePosition, uiCam))
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 드래그 상태 전이 처리 (점프 방지)
|
|
||||||
if (Input.GetMouseButtonDown(2)) { _mmbDragging = true; _mmbLastPos = Input.mousePosition; }
|
|
||||||
if (Input.GetMouseButtonUp(2)) { _mmbDragging = false; }
|
|
||||||
if (Input.GetMouseButtonDown(1)) { _rmbDragging = true; _rmbStartPos = Input.mousePosition; _yawStart = _yaw; _pitchStart = _pitch; if (_root != null) { _modelStartRot = _root.transform.rotation; _modelStartPos = _root.transform.position; } _rmbPivot = _orbitTarget; }
|
|
||||||
if (Input.GetMouseButtonUp(1)) { _rmbDragging = false; }
|
|
||||||
|
|
||||||
// 가운데 버튼: 모델 이동 (좌/우/상/하) - 카메라 기준, 픽셀 델타 기반
|
|
||||||
if (_mmbDragging && _root != null)
|
|
||||||
{
|
|
||||||
Vector3 cur = Input.mousePosition;
|
|
||||||
Vector2 dp = (Vector2)(cur - _mmbLastPos);
|
|
||||||
_mmbLastPos = cur;
|
|
||||||
// 뷰 사각 픽셀 크기 구하기
|
|
||||||
float wPix, hPix;
|
|
||||||
if (outputImage != null)
|
|
||||||
{ var r = outputImage.rectTransform.rect; wPix = Mathf.Max(1f, r.width); hPix = Mathf.Max(1f, r.height); }
|
|
||||||
else
|
|
||||||
{ var rt = GetComponent<RectTransform>(); var r = (rt != null) ? rt.rect : new Rect(0, 0, Screen.width, Screen.height); wPix = Mathf.Max(1f, r.width); hPix = Mathf.Max(1f, r.height); }
|
|
||||||
// 카메라에서의 월드/픽셀 스케일(거리 기반)
|
|
||||||
float halfV = Mathf.Tan(_viewCamera.fieldOfView * Mathf.Deg2Rad * 0.5f) * _orbitDistance;
|
|
||||||
float halfH = halfV * _viewCamera.aspect;
|
|
||||||
float worldPerPixelX = (halfH * 2f) / wPix * panSpeed;
|
|
||||||
float worldPerPixelY = (halfV * 2f) / hPix * panSpeed;
|
|
||||||
Vector3 deltaModel = _viewCamera.transform.right * (dp.x * worldPerPixelX) + _viewCamera.transform.up * (dp.y * worldPerPixelY);
|
|
||||||
_root.transform.position += deltaModel;
|
|
||||||
_orbitTarget += deltaModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 마우스 휠: 모델 전/후 이동(카메라 기준)
|
|
||||||
float scroll = Input.GetAxis("Mouse ScrollWheel");
|
|
||||||
if (Mathf.Abs(scroll) > 1e-5f && _root != null)
|
|
||||||
{
|
|
||||||
var forward = _viewCamera.transform.forward;
|
|
||||||
Vector3 deltaZ = forward * (-scroll * zoomSpeed);
|
|
||||||
_root.transform.position += deltaZ;
|
|
||||||
_orbitTarget += deltaZ;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 오른쪽 버튼: 모델 회전 (카메라 기준 yaw/pitch) - 모델 중심(_orbitTarget) 기준으로 회전
|
|
||||||
if (_rmbDragging && _root != null)
|
|
||||||
{
|
|
||||||
Vector3 cur = Input.mousePosition;
|
|
||||||
Vector2 dpAbs = (Vector2)(cur - _rmbStartPos);
|
|
||||||
// 반전: 좌우/상하 모두 반대 방향으로 회전
|
|
||||||
float yaw = -dpAbs.x * rotateDegPerPixel; // 좌우 반전
|
|
||||||
float pitch = dpAbs.y * rotateDegPerPixel; // 위아래 반전
|
|
||||||
|
|
||||||
// 카메라 기준 축으로 회전 행렬 구성
|
|
||||||
Quaternion yawQ = Quaternion.AngleAxis(yaw, _viewCamera.transform.up);
|
|
||||||
Quaternion pitchQ = Quaternion.AngleAxis(pitch, _viewCamera.transform.right);
|
|
||||||
Quaternion r = yawQ * pitchQ;
|
|
||||||
|
|
||||||
// 피벗(_rmbPivot, 보통 모델 중심) 기준으로 위치+회전 동시 적용
|
|
||||||
Vector3 startVec = _modelStartPos - _rmbPivot;
|
|
||||||
_root.transform.position = _rmbPivot + r * startVec;
|
|
||||||
_root.transform.rotation = r * _modelStartRot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 이 뷰가 보유한 런타임 리소스(RT, 인스턴스 머티리얼, 루트 GO)를 해제합니다.
|
|
||||||
/// 여러 번 호출해도 안전합니다.
|
|
||||||
/// </summary>
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
// Destroy instanced materials and restore original shared materials
|
|
||||||
foreach (var kv in _originalSharedByRenderer)
|
|
||||||
{
|
|
||||||
var r = kv.Key;
|
|
||||||
if (r == null) continue;
|
|
||||||
var originals = kv.Value;
|
|
||||||
var mats = r.materials;
|
|
||||||
for (int m = 0; m < mats.Length; m++)
|
|
||||||
{
|
|
||||||
if (mats[m] != null) UnityEngine.Object.Destroy(mats[m]);
|
|
||||||
}
|
|
||||||
r.materials = originals; // restore to originals in one go
|
|
||||||
}
|
|
||||||
_originalSharedByRenderer.Clear();
|
|
||||||
_idToObject.Clear();
|
|
||||||
if (_root != null) UnityEngine.Object.Destroy(_root);
|
|
||||||
_root = null;
|
|
||||||
_focusedId = null;
|
|
||||||
_wireframeApplied = false;
|
|
||||||
|
|
||||||
if (_viewCamera != null)
|
|
||||||
{
|
|
||||||
// RT 바인딩 해제하고 메인 디스플레이로 대체 출력되지 않도록 비활성화
|
|
||||||
if (_rt != null && _viewCamera.targetTexture == _rt)
|
|
||||||
{
|
|
||||||
_viewCamera.targetTexture = null;
|
|
||||||
}
|
|
||||||
_viewCamera.enabled = false;
|
|
||||||
}
|
|
||||||
if (_rt != null)
|
|
||||||
{
|
|
||||||
_rt.Release();
|
|
||||||
Destroy(_rt);
|
|
||||||
_rt = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDestroy()
|
|
||||||
{
|
|
||||||
// 컴포넌트 파괴 시 리소스 정리 보장
|
|
||||||
Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== 유틸리티 =====
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 지정한 GameObject와 모든 하위 오브젝트의 레이어를 일괄 설정합니다.
|
|
||||||
/// </summary>
|
|
||||||
private static void SetLayerRecursive(GameObject go, int layer)
|
|
||||||
{
|
|
||||||
if (layer < 0 || layer > 31) return;
|
|
||||||
go.layer = layer;
|
|
||||||
foreach (Transform c in go.transform) SetLayerRecursive(c.gameObject, layer);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 루트 아래 모든 렌더러의 경계(bounds)를 계산합니다.
|
|
||||||
/// </summary>
|
|
||||||
private static Bounds CalculateBounds(GameObject root)
|
|
||||||
{
|
|
||||||
var rends = root.GetComponentsInChildren<Renderer>(true);
|
|
||||||
var has = false;
|
|
||||||
var bounds = new Bounds(root.transform.position, Vector3.zero);
|
|
||||||
foreach (var r in rends)
|
|
||||||
{
|
|
||||||
if (r == null) continue;
|
|
||||||
if (!has) { bounds = r.bounds; has = true; }
|
|
||||||
else bounds.Encapsulate(r.bounds);
|
|
||||||
}
|
|
||||||
if (!has) bounds = new Bounds(root.transform.position, Vector3.one);
|
|
||||||
return bounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 카메라를 주어진 경계에 맞게 배치하고 오빗 타깃을 갱신합니다.
|
|
||||||
/// </summary>
|
|
||||||
private void FrameToBounds(Bounds b)
|
|
||||||
{
|
|
||||||
if (_viewCamera == null) return;
|
|
||||||
var center = b.center;
|
|
||||||
var extents = b.extents;
|
|
||||||
float radius = Mathf.Max(extents.x, Mathf.Max(extents.y, Mathf.Max(extents.z, 0.001f)));
|
|
||||||
float fovRad = _viewCamera.fieldOfView * Mathf.Deg2Rad;
|
|
||||||
float dist = radius / Mathf.Sin(fovRad * 0.5f);
|
|
||||||
dist = Mathf.Clamp(dist, 1f, 1e4f);
|
|
||||||
_viewCamera.transform.position = center + new Vector3(1, 0.5f, 1).normalized * dist;
|
|
||||||
_viewCamera.transform.LookAt(center);
|
|
||||||
|
|
||||||
_orbitTarget = center;
|
|
||||||
_orbitDistance = Vector3.Distance(_viewCamera.transform.position, _orbitTarget);
|
|
||||||
var dir = (_viewCamera.transform.position - _orbitTarget).normalized;
|
|
||||||
_yaw = Mathf.Atan2(dir.x, dir.z) * Mathf.Rad2Deg;
|
|
||||||
_pitch = Mathf.Asin(dir.y) * Mathf.Rad2Deg;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 루트 기준으로 트랜스폼의 전체 경로 문자열을 생성합니다. 예: "/Root/PartA/Bolt01"
|
|
||||||
/// </summary>
|
|
||||||
private string BuildFullPath(Transform node, Transform root)
|
|
||||||
{
|
|
||||||
var stack = new Stack<string>();
|
|
||||||
var current = node;
|
|
||||||
while (current != null && current != root)
|
|
||||||
{
|
|
||||||
stack.Push(current.name);
|
|
||||||
current = current.parent;
|
|
||||||
}
|
|
||||||
return "/" + string.Join('/', stack);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 서브트리의 모든 렌더러에 대해 원본 sharedMaterials를 캐싱합니다.
|
|
||||||
/// </summary>
|
|
||||||
private void CacheOriginalMaterials(GameObject go)
|
|
||||||
{
|
|
||||||
var rends = go.GetComponentsInChildren<Renderer>(true);
|
|
||||||
if (rends.Length == 0) return;
|
|
||||||
for (int i = 0; i < rends.Length; i++)
|
|
||||||
{
|
|
||||||
var r = rends[i];
|
|
||||||
if (r == null) continue;
|
|
||||||
if (_originalSharedByRenderer.ContainsKey(r)) continue; // 이미 상위에서 캐시됨
|
|
||||||
_originalSharedByRenderer[r] = r.sharedMaterials;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 6f867baed0364ea4bb3ce7a26ce84bdf
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using Cysharp.Threading.Tasks;
|
using Cysharp.Threading.Tasks;
|
||||||
using DG.Tweening;
|
using DG.Tweening;
|
||||||
using SHI.modal;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -432,7 +431,7 @@ namespace UVC.UI.Window
|
|||||||
foreach (var r in results)
|
foreach (var r in results)
|
||||||
{
|
{
|
||||||
var cloned = r.Clone();
|
var cloned = r.Clone();
|
||||||
treeListSearch.AddItem<ModelDetailListItem>(cloned);
|
treeListSearch.AddItem<TreeListItem>(cloned);
|
||||||
if (cloned.IsSelected)
|
if (cloned.IsSelected)
|
||||||
{
|
{
|
||||||
//선택된 항목은 펼치기
|
//선택된 항목은 펼치기
|
||||||
|
|||||||
Reference in New Issue
Block a user