playbar 추가, 모델 여러개 로드

This commit is contained in:
logonkhi
2025-12-02 21:09:37 +09:00
parent bc4056b474
commit 5704b2d109
46 changed files with 3009 additions and 640 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: f019cda87b206f84098dd38575768a72
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 256
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WindowsStoreApps
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 B

View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 25aaebf107892914cada8d94caeef0b2
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WindowsStoreApps
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 9e72c11031ee853409b2890dfe74d9cb
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WindowsStoreApps
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 989509807ae628144bf9ec45a2dc4397
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WindowsStoreApps
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 67a53c30e0da69e4e899c7dddfaff16e
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WindowsStoreApps
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

View File

@@ -0,0 +1,143 @@
fileFormatVersion: 2
guid: 5a70228c7e2d0fa4fb56924dca7cf261
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WindowsStoreApps
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,68 +1,34 @@
/*
* ISOPModal.uss - ISOP 모달 전역 스타일시트
*
* [개요]
* ISOP 모달 창 전체에 적용되는 스타일을 정의합니다.
* 주로 스크롤바 커스터마이징과 기본 텍스트 스타일을 포함합니다.
*
* [스타일 구조]
* 1. .unity-text-element : 기본 텍스트 스타일 (Pretendard 폰트, 10px)
* 2. .unity-scroller--vertical : 세로 스크롤바 커스터마이징
* 3. .unity-scroller--horizontal : 가로 스크롤바 커스터마이징
*
* [스크롤바 디자인]
* - 너비/높이: 6px (슬림한 디자인)
* - 트랙: 흰색 배경 (#FFFFFF)
* - 드래거: 밝은 회색 (#D8D8D8), 둥근 모서리 (3px)
* - 화살표 버튼: 숨김 처리
*
* [폰트 설정]
* - 기본 폰트: Pretendard-Regular
* - 기본 색상: rgb(34, 34, 34) - 진한 회색
* - 기본 크기: 10px
*
* [연관 파일]
* - ISOPModal.uxml : 메인 레이아웃
* - ISOPChart.uss : 간트 차트 스타일
* - ISOPModelView.uss : 모델 뷰 스타일
*/
/* ===================================
기본 텍스트 스타일
모달 내 모든 텍스트 요소에 적용
=================================== */
.unity-text-element { .unity-text-element {
-unity-font: url('project://database/Assets/Fonts/Pretendard-Regular.otf'); -unity-font: url('project://database/Assets/Fonts/Pretendard-Regular.otf');
-unity-font-definition: resource('Fonts/Pretendard/Pretendard-Regular'); -unity-font-definition: resource('Fonts/Pretendard/Pretendard-Regular');
color: rgb(34, 34, 34); /* 진한 회색 텍스트 */ color: rgb(34, 34, 34);
font-size: 10px; font-size: 10px;
} }
/* ===================================
세로 스크롤바 스타일
슬림한 6px 너비의 커스텀 스크롤바
=================================== */
.unity-scroller--vertical { .unity-scroller--vertical {
width: 6px; /* 슬림한 너비 */
margin-right: 4px;
margin-bottom: 0px;
}
/* 세로 스크롤바 트랙 (배경) */
.unity-scroller--vertical .unity-base-slider__tracker {
background-color: rgb(255, 255, 255); /* 흰색 배경 */
border-width: 0;
}
/* 세로 스크롤바 드래거 (핸들) */
.unity-scroller--vertical .unity-base-slider__dragger {
background-color: rgb(216, 216, 216); /* 밝은 회색 */
border-width: 0;
border-radius: 3px; /* 둥근 모서리 */
width: 6px; width: 6px;
margin-right: 4px;
margin-bottom: 0;
}
.unity-scroller--vertical .unity-base-slider__tracker {
background-color: rgb(255, 255, 255);
border-width: 0;
}
.unity-scroller--vertical .unity-base-slider__drag-container {
left: 0;
right: 0;
}
.unity-scroller--vertical .unity-base-slider__dragger {
background-color: rgb(216, 216, 216);
border-width: 0;
border-radius: 3px;
width: 6px;
left: 0;
} }
/* 세로 스크롤바 화살표 버튼 숨김 */
.unity-scroller--vertical .unity-repeat-button { .unity-scroller--vertical .unity-repeat-button {
display: none; display: none;
width: 0; width: 0;
@@ -71,36 +37,39 @@
min-height: 0; min-height: 0;
} }
/* 세로 스크롤바 슬라이더 마진 제거 */
.unity-scroller--vertical .unity-slider { .unity-scroller--vertical .unity-slider {
margin: 0; margin: 0;
} }
/* =================================== .unity-scroller--vertical .unity-base-field__input {
가로 스크롤바 스타일 width: 6px;
슬림한 6px 높이의 커스텀 스크롤바 min-width: 6px;
=================================== */ }
.unity-scroller--horizontal { .unity-scroller--horizontal {
height: 6px; /* 슬림한 높이 */
margin-bottom: 4px;
margin-right: 0px;
}
/* 가로 스크롤바 트랙 (배경) */
.unity-scroller--horizontal .unity-base-slider__tracker {
background-color: rgb(255, 255, 255); /* 흰색 배경 */
border-width: 0;
}
/* 가로 스크롤바 드래거 (핸들) */
.unity-scroller--horizontal .unity-base-slider__dragger {
background-color: rgb(216, 216, 216); /* 밝은 회색 */
border-width: 0;
border-radius: 3px; /* 둥근 모서리 */
height: 6px; height: 6px;
margin-bottom: 4px;
margin-right: 0;
}
.unity-scroller--horizontal .unity-base-slider__tracker {
background-color: rgb(255, 255, 255);
border-width: 0;
}
.unity-scroller--horizontal .unity-base-slider__drag-container {
top: 0;
bottom: 0;
}
.unity-scroller--horizontal .unity-base-slider__dragger {
background-color: rgb(216, 216, 216);
border-width: 0;
border-radius: 3px;
height: 6px;
top: 0;
} }
/* 가로 스크롤바 화살표 버튼 숨김 */
.unity-scroller--horizontal .unity-repeat-button { .unity-scroller--horizontal .unity-repeat-button {
display: none; display: none;
width: 0; width: 0;
@@ -109,7 +78,48 @@
min-height: 0; min-height: 0;
} }
/* 가로 스크롤바 슬라이더 마진 제거 */
.unity-scroller--horizontal .unity-slider { .unity-scroller--horizontal .unity-slider {
margin: 0; margin: 0;
} }
.unity-scroller--horizontal .unity-base-field__input {
height: 6px;
min-height: 6px;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
display: none;
}
.loading-overlay.visible {
display: flex;
}
.loading-spinner {
width: 24px;
height: 24px;
border-top-width: 4px;
border-right-width: 4px;
border-bottom-width: 4px;
border-left-width: 4px;
border-top-color: rgba(255, 255, 255, 0.3);
border-right-color: rgba(255, 255, 255, 0.3);
border-bottom-color: rgba(255, 255, 255, 0.3);
border-left-color: rgb(255, 255, 255);
border-top-left-radius: 50%;
border-top-right-radius: 50%;
border-bottom-left-radius: 50%;
border-bottom-right-radius: 50%;
align-self: center;
align-content: center;
justify-content: center;
align-items: center;
}

View File

@@ -1,72 +1,17 @@
<!--
ISOPModal.uxml - ISOP(조선소 공정 계획) 모달 메인 레이아웃
[개요]
ISOP 모달 창의 전체 레이아웃을 정의합니다.
TreeList(모델 계층), ISOPModelView(3D 뷰어), ISOPChart(간트 차트) 세 가지 뷰를
수평으로 배치합니다.
[UI 구조]
Root
├── topbar - 상단 헤더 바 (제목, 닫기 버튼)
│ ├── title - 모달 제목 라벨
│ └── closeButton - 모달 닫기 버튼
└── content - 메인 컨텐츠 영역 (수평 배치)
├── TreeList - 모델 계층 구조 (왼쪽 사이드바)
├── ISOPModelView - 3D 모델 뷰어 (중앙)
├── ISOPChart - 간트 차트 (오른쪽)
├── show-tree-btn - TreeList 표시 버튼 (숨겨진 상태일 때)
└── drag-btn - ModelView/Chart 크기 조절 드래그 버튼
[커스텀 컴포넌트]
- SHI.Modal.TreeList : 모델 계층 트리뷰
- SHI.Modal.ISOP.ISOPModelView : 3D 모델 렌더링 뷰
- SHI.Modal.ISOP.ISOPChart : 간트 차트 뷰
[레이아웃 특징]
- 수평(Horizontal) 레이아웃: TreeList | ModelView | Chart
- flex-basis: 0으로 ModelView와 Chart가 동일한 공간 차지
- drag-btn으로 ModelView/Chart 비율 조절 가능
[연관 파일]
- ISOPModal.uss : 스타일시트 (스크롤바 커스터마이징)
- ISOPModal.cs : C# 컨트롤러 (뷰 간 연동 처리)
[사용되는 리소스]
- Fonts/Pretendard/Pretendard-Bold : 제목 폰트
- SHI/Images/btn_close_24 : 닫기 버튼 이미지
- SHI/Images/btn_show_explorer_40x41 : TreeList 표시 버튼 이미지
- SHI/Images/btn_drag_40 : 드래그 버튼 이미지
-->
<ui:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False"> <ui:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
<Style src="project://database/Assets/Resources/SHI/Modal/ISOP/ISOPModal.uss?fileID=7433441132597879392&amp;guid=b798684be75686146a13288df44a17a3&amp;type=3#ISOPModal" /> <Style src="project://database/Assets/Resources/SHI/Modal/ISOP/ISOPModal.uss?fileID=7433441132597879392&amp;guid=b798684be75686146a13288df44a17a3&amp;type=3#ISOPModal" />
<!-- 상단 헤더 바: 제목과 닫기 버튼 -->
<ui:VisualElement name="topbar" style="flex-grow: 0; width: 100%; height: 60px; background-color: rgb(9, 24, 53); flex-shrink: 0; align-items: center; align-self: auto; align-content: auto; flex-direction: row; justify-content: center;"> <ui:VisualElement name="topbar" style="flex-grow: 0; width: 100%; height: 60px; background-color: rgb(9, 24, 53); flex-shrink: 0; align-items: center; align-self: auto; align-content: auto; flex-direction: row; justify-content: center;">
<!-- 모달 제목 (중앙 정렬) -->
<ui:Label text="블록2 상세" name="title" style="-unity-font-definition: url(&quot;project://database/Assets/Resources/Fonts/Pretendard/Pretendard-Bold.otf?fileID=12800000&amp;guid=f9a95f3332ad9a9468b4eaa89f125b35&amp;type=3#Pretendard-Bold&quot;); font-size: 24px; color: rgb(255, 255, 255); flex-basis: auto; justify-content: center; align-items: center; -unity-text-align: middle-center; width: 500px;" /> <ui:Label text="블록2 상세" name="title" style="-unity-font-definition: url(&quot;project://database/Assets/Resources/Fonts/Pretendard/Pretendard-Bold.otf?fileID=12800000&amp;guid=f9a95f3332ad9a9468b4eaa89f125b35&amp;type=3#Pretendard-Bold&quot;); font-size: 24px; color: rgb(255, 255, 255); flex-basis: auto; justify-content: center; align-items: center; -unity-text-align: middle-center; width: 500px;" />
<!-- 닫기 버튼 (오른쪽 상단) -->
<ui:Button name="closeButton" style="width: 24px; height: 24px; background-image: url(&quot;project://database/Assets/Resources/SHI/Images/btn_close_24.png?fileID=21300000&amp;guid=461a84aa7f4a0104bb7a41e1f9d15f86&amp;type=3#btn_close_24&quot;); background-color: rgba(255, 255, 255, 0); border-top-width: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0; position: absolute; right: 18px;" /> <ui:Button name="closeButton" style="width: 24px; height: 24px; background-image: url(&quot;project://database/Assets/Resources/SHI/Images/btn_close_24.png?fileID=21300000&amp;guid=461a84aa7f4a0104bb7a41e1f9d15f86&amp;type=3#btn_close_24&quot;); background-color: rgba(255, 255, 255, 0); border-top-width: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0; position: absolute; right: 18px;" />
</ui:VisualElement> </ui:VisualElement>
<!-- 메인 컨텐츠 영역: TreeList, ModelView, Chart 수평 배치 -->
<ui:VisualElement name="content" style="flex-grow: 1; flex-shrink: 1; align-self: stretch; align-content: auto; align-items: stretch; width: 100%; flex-direction: row; overflow: hidden;"> <ui:VisualElement name="content" style="flex-grow: 1; flex-shrink: 1; align-self: stretch; align-content: auto; align-items: stretch; width: 100%; flex-direction: row; overflow: hidden;">
<!-- TreeList: 모델 계층 구조 트리뷰 (왼쪽 사이드바) -->
<SHI.Modal.TreeList name="treeList" style="bottom: 0; left: 0; flex-direction: column; flex-wrap: nowrap; height: 100%; flex-shrink: 0;" /> <SHI.Modal.TreeList name="treeList" style="bottom: 0; left: 0; flex-direction: column; flex-wrap: nowrap; height: 100%; flex-shrink: 0;" />
<!-- ISOPModelView: 3D 모델 렌더링 뷰어 (중앙)
flex-basis: 0으로 Chart와 동일한 공간 분할 -->
<SHI.Modal.ISOP.ISOPModelView style="flex-grow: 1; flex-shrink: 1; flex-basis: 0; flex-direction: column; min-width: 0; overflow: hidden;" /> <SHI.Modal.ISOP.ISOPModelView style="flex-grow: 1; flex-shrink: 1; flex-basis: 0; flex-direction: column; min-width: 0; overflow: hidden;" />
<!-- ISOPChart: 간트 차트 뷰 (오른쪽)
flex-basis: 0으로 ModelView와 동일한 공간 분할 -->
<SHI.Modal.ISOP.ISOPChart style="flex-grow: 1; flex-shrink: 1; flex-basis: 0; height: 100%; min-width: 0; overflow: hidden;" /> <SHI.Modal.ISOP.ISOPChart style="flex-grow: 1; flex-shrink: 1; flex-basis: 0; height: 100%; min-width: 0; overflow: hidden;" />
<!-- TreeList 표시 버튼: TreeList가 숨겨졌을 때 나타남 -->
<ui:Button text="&#10;" name="show-tree-btn" style="position: absolute; left: 0; top: 26px; width: 40px; height: 41px; margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; border-top-width: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0; background-color: rgba(255, 255, 255, 0); background-image: resource(&apos;SHI/Images/btn_show_explorer_40x41&apos;);" /> <ui:Button text="&#10;" name="show-tree-btn" style="position: absolute; left: 0; top: 26px; width: 40px; height: 41px; margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; border-top-width: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0; background-color: rgba(255, 255, 255, 0); background-image: resource(&apos;SHI/Images/btn_show_explorer_40x41&apos;);" />
<!-- 드래그 버튼: ModelView/Chart 크기 비율 조절
HorizontalDragManipulator에서 처리 -->
<ui:Button text="&#10;" name="drag-btn" style="position: absolute; left: 60%; top: auto; width: 40px; height: 40px; margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; border-top-width: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0; background-color: rgba(255, 255, 255, 0); background-image: resource(&apos;SHI/Images/btn_drag_40&apos;); bottom: 40px; align-items: center; align-self: center;" /> <ui:Button text="&#10;" name="drag-btn" style="position: absolute; left: 60%; top: auto; width: 40px; height: 40px; margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; border-top-width: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0; background-color: rgba(255, 255, 255, 0); background-image: resource(&apos;SHI/Images/btn_drag_40&apos;); bottom: 40px; align-items: center; align-self: center;" />
<ui:VisualElement name="loading-overlay" class="loading-overlay">
<ui:VisualElement name="loading-spinner" class="loading-spinner" />
</ui:VisualElement>
</ui:VisualElement> </ui:VisualElement>
</ui:UXML> </ui:UXML>

View File

@@ -0,0 +1,13 @@
/* ===================================
드롭다운 팝업 아이템 스타일
=================================== */
.unity-base-dropdown__item {
padding: 0px;
background-color: rgb(40, 44, 52);
color: rgb(255, 255, 255);
}
.unity-base-dropdown__container-inner {
padding: 0px;
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 15f8f9b790a1042468c972e5ea4e9430
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

View File

@@ -1,69 +1,34 @@
/*
* NWModal.uss - NW 모달 전역 스타일시트
*
* [개요]
* NW 모달 창 전체에 적용되는 스타일을 정의합니다.
* ISOPModal.uss와 동일한 스타일을 사용합니다.
* 주로 스크롤바 커스터마이징과 기본 텍스트 스타일을 포함합니다.
*
* [스타일 구조]
* 1. .unity-text-element : 기본 텍스트 스타일 (Pretendard 폰트, 10px)
* 2. .unity-scroller--vertical : 세로 스크롤바 커스터마이징
* 3. .unity-scroller--horizontal : 가로 스크롤바 커스터마이징
*
* [스크롤바 디자인]
* - 너비/높이: 6px (슬림한 디자인)
* - 트랙: 흰색 배경 (#FFFFFF)
* - 드래거: 밝은 회색 (#D8D8D8), 둥근 모서리 (3px)
* - 화살표 버튼: 숨김 처리
*
* [폰트 설정]
* - 기본 폰트: Pretendard-Regular
* - 기본 색상: rgb(34, 34, 34) - 진한 회색
* - 기본 크기: 10px
*
* [연관 파일]
* - NWModal.uxml : 메인 레이아웃
* - NWChart.uss : 네트워크 다이어그램 스타일
* - NWModalView.uss : 모델 뷰 스타일
*/
/* ===================================
기본 텍스트 스타일
모달 내 모든 텍스트 요소에 적용
=================================== */
.unity-text-element { .unity-text-element {
-unity-font: url('project://database/Assets/Fonts/Pretendard-Regular.otf'); -unity-font: url('project://database/Assets/Fonts/Pretendard-Regular.otf');
-unity-font-definition: resource('Fonts/Pretendard/Pretendard-Regular'); -unity-font-definition: resource('Fonts/Pretendard/Pretendard-Regular');
color: rgb(34, 34, 34); /* 진한 회색 텍스트 */ color: rgb(34, 34, 34);
font-size: 10px; font-size: 10px;
} }
/* ===================================
세로 스크롤바 스타일
슬림한 6px 너비의 커스텀 스크롤바
=================================== */
.unity-scroller--vertical { .unity-scroller--vertical {
width: 6px; /* 슬림한 너비 */
margin-right: 4px;
margin-bottom: 0px;
}
/* 세로 스크롤바 트랙 (배경) */
.unity-scroller--vertical .unity-base-slider__tracker {
background-color: rgb(255, 255, 255); /* 흰색 배경 */
border-width: 0;
}
/* 세로 스크롤바 드래거 (핸들) */
.unity-scroller--vertical .unity-base-slider__dragger {
background-color: rgb(216, 216, 216); /* 밝은 회색 */
border-width: 0;
border-radius: 3px; /* 둥근 모서리 */
width: 6px; width: 6px;
margin-right: 4px;
margin-bottom: 0;
}
.unity-scroller--vertical .unity-base-slider__tracker {
background-color: rgb(255, 255, 255);
border-width: 0;
}
.unity-scroller--vertical .unity-base-slider__drag-container {
left: 0;
right: 0;
}
.unity-scroller--vertical .unity-base-slider__dragger {
background-color: rgb(216, 216, 216);
border-width: 0;
border-radius: 3px;
width: 6px;
left: 0;
} }
/* 세로 스크롤바 화살표 버튼 숨김 */
.unity-scroller--vertical .unity-repeat-button { .unity-scroller--vertical .unity-repeat-button {
display: none; display: none;
width: 0; width: 0;
@@ -72,36 +37,39 @@
min-height: 0; min-height: 0;
} }
/* 세로 스크롤바 슬라이더 마진 제거 */
.unity-scroller--vertical .unity-slider { .unity-scroller--vertical .unity-slider {
margin: 0; margin: 0;
} }
/* =================================== .unity-scroller--vertical .unity-base-field__input {
가로 스크롤바 스타일 width: 6px;
슬림한 6px 높이의 커스텀 스크롤바 min-width: 6px;
=================================== */ }
.unity-scroller--horizontal { .unity-scroller--horizontal {
height: 6px; /* 슬림한 높이 */
margin-bottom: 4px;
margin-right: 0px;
}
/* 가로 스크롤바 트랙 (배경) */
.unity-scroller--horizontal .unity-base-slider__tracker {
background-color: rgb(255, 255, 255); /* 흰색 배경 */
border-width: 0;
}
/* 가로 스크롤바 드래거 (핸들) */
.unity-scroller--horizontal .unity-base-slider__dragger {
background-color: rgb(216, 216, 216); /* 밝은 회색 */
border-width: 0;
border-radius: 3px; /* 둥근 모서리 */
height: 6px; height: 6px;
margin-bottom: 4px;
margin-right: 0;
}
.unity-scroller--horizontal .unity-base-slider__tracker {
background-color: rgb(255, 255, 255);
border-width: 0;
}
.unity-scroller--horizontal .unity-base-slider__drag-container {
top: 0;
bottom: 0;
}
.unity-scroller--horizontal .unity-base-slider__dragger {
background-color: rgb(216, 216, 216);
border-width: 0;
border-radius: 3px;
height: 6px;
top: 0;
} }
/* 가로 스크롤바 화살표 버튼 숨김 */
.unity-scroller--horizontal .unity-repeat-button { .unity-scroller--horizontal .unity-repeat-button {
display: none; display: none;
width: 0; width: 0;
@@ -110,7 +78,48 @@
min-height: 0; min-height: 0;
} }
/* 가로 스크롤바 슬라이더 마진 제거 */
.unity-scroller--horizontal .unity-slider { .unity-scroller--horizontal .unity-slider {
margin: 0; margin: 0;
} }
.unity-scroller--horizontal .unity-base-field__input {
height: 6px;
min-height: 6px;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
display: none;
}
.loading-overlay.visible {
display: flex;
}
.loading-spinner {
width: 24px;
height: 24px;
border-top-width: 4px;
border-right-width: 4px;
border-bottom-width: 4px;
border-left-width: 4px;
border-top-color: rgba(255, 255, 255, 0.3);
border-right-color: rgba(255, 255, 255, 0.3);
border-bottom-color: rgba(255, 255, 255, 0.3);
border-left-color: rgb(255, 255, 255);
border-top-left-radius: 50%;
border-top-right-radius: 50%;
border-bottom-left-radius: 50%;
border-bottom-right-radius: 50%;
align-self: center;
align-items: center;
justify-content: center;
align-content: center;
}

View File

@@ -1,76 +1,22 @@
<!--
NWModal.uxml - NW(네트워크 다이어그램) 모달 메인 레이아웃
[개요]
NW 모달 창의 전체 레이아웃을 정의합니다.
TreeList(모델 계층), NWModelView(3D 뷰어), NWChart(네트워크 다이어그램)를
수직으로 배치합니다. (ISOP과 달리 ModelView가 위, Chart가 아래)
[UI 구조]
Root
├── topbar - 상단 헤더 바 (제목, 닫기 버튼)
│ ├── title - 모달 제목 라벨
│ └── closeButton - 모달 닫기 버튼
└── content - 메인 컨텐츠 영역 (수평 배치)
├── TreeList - 모델 계층 구조 (왼쪽 사이드바)
├── right-panel - 오른쪽 패널 (수직 배치)
│ ├── NWModelView - 3D 모델 뷰어 (상단)
│ ├── NWChart - 네트워크 다이어그램 (하단)
│ └── drag-btn - ModelView/Chart 크기 조절 드래그 버튼
└── show-tree-btn - TreeList 표시 버튼 (숨겨진 상태일 때)
[ISOP과의 차이점]
- ISOP: TreeList | ModelView | Chart (수평 3분할)
- NW: TreeList | (ModelView / Chart) (수평 2분할 + 수직 2분할)
- 드래그 버튼: ISOP=수평, NW=수직 (90도 회전)
[커스텀 컴포넌트]
- SHI.Modal.TreeList : 모델 계층 트리뷰
- SHI.Modal.NW.NWModelView : 3D 모델 렌더링 뷰
- SHI.Modal.NW.NWChart : 네트워크 다이어그램 뷰
[연관 파일]
- NWModal.uss : 스타일시트 (스크롤바 커스터마이징)
- NWModal.cs : C# 컨트롤러 (뷰 간 연동 처리)
[사용되는 리소스]
- Fonts/Pretendard/Pretendard-Bold : 제목 폰트
- SHI/Images/btn_close_24 : 닫기 버튼 이미지
- SHI/Images/btn_show_explorer_40x41 : TreeList 표시 버튼 이미지
- SHI/Images/btn_drag_40 : 드래그 버튼 이미지 (90도 회전)
-->
<ui:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False"> <ui:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
<Style src="project://database/Assets/Resources/SHI/Modal/NW/NWModal.uss?fileID=7433441132597879392&amp;guid=067929ea94907c248a545d139eb77664&amp;type=3#NWModal" /> <Style src="project://database/Assets/Resources/SHI/Modal/NW/NWModal.uss?fileID=7433441132597879392&amp;guid=067929ea94907c248a545d139eb77664&amp;type=3#NWModal" />
<!-- 상단 헤더 바: 제목과 닫기 버튼 -->
<ui:VisualElement name="topbar" style="flex-grow: 0; width: 100%; height: 60px; background-color: rgb(9, 24, 53); flex-shrink: 0; align-items: center; align-self: auto; align-content: auto; flex-direction: row; justify-content: center;"> <ui:VisualElement name="topbar" style="flex-grow: 0; width: 100%; height: 60px; background-color: rgb(9, 24, 53); flex-shrink: 0; align-items: center; align-self: auto; align-content: auto; flex-direction: row; justify-content: center;">
<!-- 모달 제목 (중앙 정렬) -->
<ui:Label text="블록2 상세" name="title" style="-unity-font-definition: url(&quot;project://database/Assets/Resources/Fonts/Pretendard/Pretendard-Bold.otf?fileID=12800000&amp;guid=f9a95f3332ad9a9468b4eaa89f125b35&amp;type=3#Pretendard-Bold&quot;); font-size: 24px; color: rgb(255, 255, 255); flex-basis: auto; justify-content: center; align-items: center; -unity-text-align: middle-center; width: 500px;" /> <ui:Label text="블록2 상세" name="title" style="-unity-font-definition: url(&quot;project://database/Assets/Resources/Fonts/Pretendard/Pretendard-Bold.otf?fileID=12800000&amp;guid=f9a95f3332ad9a9468b4eaa89f125b35&amp;type=3#Pretendard-Bold&quot;); font-size: 24px; color: rgb(255, 255, 255); flex-basis: auto; justify-content: center; align-items: center; -unity-text-align: middle-center; width: 500px;" />
<!-- 닫기 버튼 (오른쪽 상단) -->
<ui:Button name="closeButton" style="width: 24px; height: 24px; background-image: url(&quot;project://database/Assets/Resources/SHI/Images/btn_close_24.png?fileID=21300000&amp;guid=461a84aa7f4a0104bb7a41e1f9d15f86&amp;type=3#btn_close_24&quot;); background-color: rgba(255, 255, 255, 0); border-top-width: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0; position: absolute; right: 18px;" /> <ui:Button name="closeButton" style="width: 24px; height: 24px; background-image: url(&quot;project://database/Assets/Resources/SHI/Images/btn_close_24.png?fileID=21300000&amp;guid=461a84aa7f4a0104bb7a41e1f9d15f86&amp;type=3#btn_close_24&quot;); background-color: rgba(255, 255, 255, 0); border-top-width: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0; position: absolute; right: 18px;" />
</ui:VisualElement> </ui:VisualElement>
<!-- 메인 컨텐츠 영역: TreeList + 오른쪽 패널 -->
<ui:VisualElement name="content" style="flex-grow: 1; flex-shrink: 1; align-self: stretch; align-content: auto; align-items: stretch; width: 100%; flex-direction: row; overflow: hidden;"> <ui:VisualElement name="content" style="flex-grow: 1; flex-shrink: 1; align-self: stretch; align-content: auto; align-items: stretch; width: 100%; flex-direction: row; overflow: hidden;">
<!-- TreeList: 모델 계층 구조 트리뷰 (왼쪽 사이드바) -->
<SHI.Modal.TreeList name="treeList" style="bottom: 0; left: 0; flex-direction: column; flex-wrap: nowrap; height: 100%; flex-shrink: 0;" /> <SHI.Modal.TreeList name="treeList" style="bottom: 0; left: 0; flex-direction: column; flex-wrap: nowrap; height: 100%; flex-shrink: 0;" />
<!-- 오른쪽 패널: ModelView(상단) + Chart(하단) 수직 배치 -->
<ui:VisualElement name="right-panel" style="flex-grow: 1; flex-shrink: 1; flex-direction: column; min-width: 0; overflow: hidden;"> <ui:VisualElement name="right-panel" style="flex-grow: 1; flex-shrink: 1; flex-direction: column; min-width: 0; overflow: hidden;">
<!-- NWModelView: 3D 모델 렌더링 뷰어 (상단)
flex-basis: 0으로 Chart와 동일한 공간 분할 -->
<SHI.Modal.NW.NWModelView style="flex-grow: 1; flex-shrink: 1; flex-basis: 0; flex-direction: column; min-width: 0; min-height: 0; overflow: hidden;" /> <SHI.Modal.NW.NWModelView style="flex-grow: 1; flex-shrink: 1; flex-basis: 0; flex-direction: column; min-width: 0; min-height: 0; overflow: hidden;" />
<!-- NWChart: 네트워크 다이어그램 뷰 (하단)
flex-basis: 0으로 ModelView와 동일한 공간 분할 -->
<SHI.Modal.NW.NWChart style="flex-grow: 1; flex-shrink: 1; flex-basis: 0; min-width: 0; min-height: 0; overflow: hidden;" /> <SHI.Modal.NW.NWChart style="flex-grow: 1; flex-shrink: 1; flex-basis: 0; min-width: 0; min-height: 0; overflow: hidden;" />
<!-- 드래그 버튼: ModelView/Chart 크기 비율 조절 (수직)
rotate: 90deg로 수직 드래그용으로 회전 -->
<ui:Button text="&#10;" name="drag-btn" style="position: absolute; width: 40px; height: 40px; margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: -20px; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; border-top-width: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0; background-color: rgba(255, 255, 255, 0); background-image: resource(&apos;SHI/Images/btn_drag_40&apos;); align-items: center; align-self: center; right: 10px; top: 47%; rotate: 90deg;" /> <ui:Button text="&#10;" name="drag-btn" style="position: absolute; width: 40px; height: 40px; margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: -20px; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; border-top-width: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0; background-color: rgba(255, 255, 255, 0); background-image: resource(&apos;SHI/Images/btn_drag_40&apos;); align-items: center; align-self: center; right: 10px; top: 47%; rotate: 90deg;" />
</ui:VisualElement> </ui:VisualElement>
<!-- TreeList 표시 버튼: TreeList가 숨겨졌을 때 나타남 -->
<ui:Button text="&#10;" name="show-tree-btn" style="position: absolute; left: 0; top: 26px; width: 40px; height: 41px; margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; border-top-width: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0; background-color: rgba(255, 255, 255, 0); background-image: resource(&apos;SHI/Images/btn_show_explorer_40x41&apos;);" /> <ui:Button text="&#10;" name="show-tree-btn" style="position: absolute; left: 0; top: 26px; width: 40px; height: 41px; margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; border-top-width: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0; background-color: rgba(255, 255, 255, 0); background-image: resource(&apos;SHI/Images/btn_show_explorer_40x41&apos;);" />
<SHI.Modal.PlayBar name="play-bar" style="position: absolute; bottom: 54px; right: 62px; left: 302px;" />
<!-- 로딩 오버레이: 데이터 로딩 중 표시 -->
<ui:VisualElement name="loading-overlay" class="loading-overlay">
<ui:VisualElement name="loading-spinner" class="loading-spinner" />
</ui:VisualElement>
</ui:VisualElement> </ui:VisualElement>
</ui:UXML> </ui:UXML>

View File

@@ -40,7 +40,7 @@
<!-- 3D 렌더링 컨테이너: RenderTexture가 배경으로 표시됨 <!-- 3D 렌더링 컨테이너: RenderTexture가 배경으로 표시됨
- flex-grow: 1로 부모 영역 전체 채움 - flex-grow: 1로 부모 영역 전체 채움
- overflow: hidden으로 내용 잘림 방지 --> - overflow: hidden으로 내용 잘림 방지 -->
<ui:VisualElement name="render-container" style="flex-grow: 1; height: 100%; min-width: 0; overflow: hidden;" /> <ui:VisualElement name="render-container" style="flex-grow: 1; flex-shrink: 1; flex-basis: 0; min-width: 0; min-height: 0; overflow: hidden;" />
<!-- 확장 버튼: 모델 뷰를 전체 화면으로 확장 <!-- 확장 버튼: 모델 뷰를 전체 화면으로 확장
- 왼쪽 하단에 위치 (ISOPModelView는 오른쪽 상단) --> - 왼쪽 하단에 위치 (ISOPModelView는 오른쪽 상단) -->

View File

@@ -0,0 +1,152 @@
.playbar-container {
background-color: rgba(255, 255, 255, 0);
padding-top: 15px;
padding-right: 20px;
padding-bottom: 15px;
padding-left: 20px;
flex-direction: column;
width: 100%;
height: 130px;
background-image: resource('SHI/Images/playbar_bg');
}
.progress-section {
flex-direction: row;
align-items: center;
margin-bottom: 10px;
}
.date-label {
color: rgb(255, 255, 255);
font-size: 14px;
-unity-font-definition: resource('Fonts/Pretendard/Pretendard-Regular');
min-width: 70px;
}
.start-label {
-unity-text-align: middle-left;
margin-right: 10px;
}
.end-label {
-unity-text-align: middle-right;
margin-left: 10px;
}
.progress-track {
flex-grow: 1;
height: 4px;
background-color: rgba(0, 0, 0, 0.3);
position: relative;
}
.progress-fill {
position: absolute;
left: 0;
top: 0;
height: 100%;
background-color: rgb(0, 70, 140);
width: 50%;
}
.controls-section {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.button-group {
flex-direction: row;
align-items: center;
}
.control-btn {
width: 24px;
height: 24px;
margin-top: 0;
margin-right: 4px;
margin-bottom: 0;
margin-left: 4px;
padding-top: 0;
padding-right: 0;
padding-bottom: 0;
padding-left: 0;
border-top-width: 0;
border-right-width: 0;
border-bottom-width: 0;
border-left-width: 0;
background-color: rgba(0, 0, 0, 0);
-unity-background-scale-mode: scale-to-fit;
}
.control-btn:hover {
background-color: rgba(0, 0, 0, 0.1);
}
.control-btn:active {
background-color: rgba(0, 0, 0, 0.2);
}
.play-btn {
background-image: resource('SHI/Images/playbar_icon_play_17x21');
}
.play-btn.playing {
background-image: resource('SHI/Images/playbar_icon_pause_17');
}
.stop-btn {
background-image: resource('SHI/Images/playbar_icon_stop_17');
}
.first-btn {
background-image: resource('SHI/Images/playbar_icon_start_16x21');
}
.last-btn {
background-image: resource('SHI/Images/playbar_icon_end_16x21');
}
.current-time-label {
color: rgb(255, 255, 255);
font-size: 14px;
-unity-font-definition: resource('Fonts/Pretendard/Pretendard-SemiBold');
-unity-text-align: middle-center;
}
.interval-dropdown {
width: 60px;
height: 24px;
background-color: rgb(255, 255, 255);
border-radius: 4px;
border-top-width: 1px;
border-right-width: 1px;
border-bottom-width: 1px;
border-left-width: 1px;
border-color: rgb(180, 180, 180);
color: rgb(50, 50, 50);
font-size: 12px;
-unity-font-definition: resource('Fonts/Pretendard/Pretendard-Regular');
}
.interval-dropdown .unity-base-popup-field__input {
background-color: rgb(255, 255, 255);
border-width: 0;
padding-left: 4px;
padding-top: 0;
padding-right: 8px;
padding-bottom: 0;
}
.interval-dropdown .unity-base-popup-field__text {
color: rgb(50, 50, 50);
-unity-text-align: middle-left;
margin-top: 0;
margin-right: 0;
margin-bottom: 0;
margin-left: 0;
}
.interval-dropdown .unity-base-popup-field__arrow {
-unity-background-image-tint-color: rgb(100, 100, 100);
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 21091244fd5f49d42a6694b75fe2c395
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

View File

@@ -0,0 +1,22 @@
<ui:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
<Style src="project://database/Assets/Resources/SHI/Modal/PlayBar.uss?fileID=7433441132597879392&amp;guid=21091244fd5f49d42a6694b75fe2c395&amp;type=3#PlayBar" />
<ui:VisualElement name="playbar-container" class="playbar-container">
<ui:VisualElement name="progress-section" class="progress-section">
<ui:Label name="start-label" text="2025-09-01" class="date-label start-label" />
<ui:VisualElement name="progress-track" class="progress-track">
<ui:VisualElement name="progress-fill" class="progress-fill" />
</ui:VisualElement>
<ui:Label name="end-label" text="2025-09-01" class="date-label end-label" />
</ui:VisualElement>
<ui:VisualElement name="controls-section" class="controls-section">
<ui:VisualElement name="button-group" class="button-group">
<ui:Button name="play-btn" class="control-btn play-btn" style="width: 17px; height: 21px;" />
<ui:Button name="first-btn" class="control-btn first-btn" style="width: 16px; height: 21px;" />
<ui:Button name="last-btn" class="control-btn last-btn" style="width: 16px; height: 21px;" />
<ui:Button name="stop-btn" class="control-btn stop-btn" style="width: 17px; height: 17px;" />
</ui:VisualElement>
<ui:Label name="current-time-label" text="2025-09-01 13:56" class="current-time-label" />
<ui:DropdownField name="interval-dropdown" index="-1" class="interval-dropdown" />
</ui:VisualElement>
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 92fab9ded97bb07428895b8a3bd766dc
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -150,3 +150,105 @@
.visibility-off { .visibility-off {
background-image: resource('SHI/Images/icon_visibility_off_64'); background-image: resource('SHI/Images/icon_visibility_off_64');
} }
/* ===================================
세로 스크롤바 스타일
슬림한 6px 너비의 커스텀 스크롤바
=================================== */
.unity-scroller--vertical {
width: 6px; /* 슬림한 너비 */
margin-right: 4px;
margin-bottom: 0px;
}
/* 세로 스크롤바 트랙 (배경) */
.unity-scroller--vertical .unity-base-slider__tracker {
background-color: rgba(255, 255, 255, 0); /* 흰색 배경 */
border-width: 0;
}
/* 세로 스크롤바 드래거 컨테이너 위치 조정 */
.unity-scroller--vertical .unity-base-slider__drag-container {
left: 0;
right: 0;
}
/* 세로 스크롤바 드래거 (핸들) */
.unity-scroller--vertical .unity-base-slider__dragger {
background-color: rgb(216, 216, 216); /* 밝은 회색 */
border-width: 0;
border-radius: 3px; /* 둥근 모서리 */
width: 6px;
left: 0;
}
/* 세로 스크롤바 화살표 버튼 숨김 */
.unity-scroller--vertical .unity-repeat-button {
display: none;
width: 0;
height: 0;
min-width: 0;
min-height: 0;
}
/* 세로 스크롤바 슬라이더 마진 제거 */
.unity-scroller--vertical .unity-slider {
margin: 0;
}
/* 세로 스크롤바 입력 필드 크기 조정 */
.unity-scroller--vertical .unity-base-field__input {
width: 6px;
min-width: 6px;
}
/* ===================================
가로 스크롤바 스타일
슬림한 6px 높이의 커스텀 스크롤바
=================================== */
.unity-scroller--horizontal {
height: 6px; /* 슬림한 높이 */
margin-bottom: 4px;
margin-right: 0px;
}
/* 가로 스크롤바 트랙 (배경) */
.unity-scroller--horizontal .unity-base-slider__tracker {
background-color: rgba(255, 255, 255, 0); /* 흰색 배경 */
border-width: 0;
}
/* 가로 스크롤바 드래거 컨테이너 위치 조정 */
.unity-scroller--horizontal .unity-base-slider__drag-container {
top: 0;
bottom: 0;
}
/* 가로 스크롤바 드래거 (핸들) */
.unity-scroller--horizontal .unity-base-slider__dragger {
background-color: rgb(216, 216, 216); /* 밝은 회색 */
border-width: 0;
border-radius: 3px; /* 둥근 모서리 */
height: 6px;
top: 0;
}
/* 가로 스크롤바 화살표 버튼 숨김 */
.unity-scroller--horizontal .unity-repeat-button {
display: none;
width: 0;
height: 0;
min-width: 0;
min-height: 0;
}
/* 가로 스크롤바 슬라이더 마진 제거 */
.unity-scroller--horizontal .unity-slider {
margin: 0;
}
/* 가로 스크롤바 입력 필드 크기 조정 */
.unity-scroller--horizontal .unity-base-field__input {
height: 6px;
min-height: 6px;
}

View File

@@ -1,57 +1,14 @@
<!--
TreeList.uxml - 계층 구조 트리 뷰 메인 레이아웃
[개요]
3D 모델의 계층 구조를 표시하는 TreeList 컴포넌트의 UI 레이아웃입니다.
검색 기능과 가시성 토글을 지원합니다.
[UI 구조]
container (tree-menu-container)
├── header - 상단 헤더 영역
│ ├── Label "모델 검색" - 제목
│ └── hide-btn - TreeList 패널 숨기기 버튼
├── search-field - 검색 입력 필드
│ ├── clear-btn - 검색어 지우기 버튼
│ └── VisualElement - 검색 아이콘
└── main-tree-view - Unity TreeView (항목들이 여기에 렌더링됨)
[연관 파일]
- TreeList.uss : 스타일시트
- TreeListItem.uxml : 개별 항목 템플릿
- TreeList.cs : C# 컨트롤러 (SHI.Modal 네임스페이스)
[사용되는 리소스]
- Fonts/Pretendard/Pretendard-Bold : 제목 폰트
- SHI/Images/btn_close_explorer_22 : 닫기 버튼 이미지
- SHI/Images/btn_cancel_64 : 검색어 지우기 버튼 이미지
- SHI/Images/icon_search_16x17 : 검색 아이콘
-->
<ui:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False"> <ui:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
<Style src="project://database/Assets/Resources/SHI/Modal/TreeList.uss?fileID=7433441132597879392&amp;guid=a1b1f50d423b463408e1f540fb4acfe9&amp;type=3#TreeList" /> <Style src="project://database/Assets/Resources/SHI/Modal/TreeList.uss?fileID=7433441132597879392&amp;guid=a1b1f50d423b463408e1f540fb4acfe9&amp;type=3#TreeList" />
<!-- 메인 컨테이너: 어두운 배경의 사이드 패널 -->
<ui:VisualElement name="container" class="tree-menu-container"> <ui:VisualElement name="container" class="tree-menu-container">
<!-- 헤더 영역: 제목과 숨기기 버튼 -->
<ui:VisualElement name="header" style="flex-direction: row; margin-bottom: 5px; justify-content: space-between;"> <ui:VisualElement name="header" style="flex-direction: row; margin-bottom: 5px; justify-content: space-between;">
<!-- 제목 라벨 -->
<ui:Label text="모델 검색" style="color: white; -unity-font-style: normal; font-size: 20px; -unity-font-definition: resource(&apos;Fonts/Pretendard/Pretendard-Bold&apos;);" /> <ui:Label text="모델 검색" style="color: white; -unity-font-style: normal; font-size: 20px; -unity-font-definition: resource(&apos;Fonts/Pretendard/Pretendard-Bold&apos;);" />
<!-- TreeList 패널 숨기기 버튼 (X 아이콘) -->
<ui:Button name="hide-btn" style="width: 22px; height: 22px; margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; background-color: rgba(188, 188, 188, 0); background-image: resource(&apos;SHI/Images/btn_close_explorer_22&apos;); align-self: center; align-items: auto; border-top-width: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0;" /> <ui:Button name="hide-btn" style="width: 22px; height: 22px; margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; background-color: rgba(188, 188, 188, 0); background-image: resource(&apos;SHI/Images/btn_close_explorer_22&apos;); align-self: center; align-items: auto; border-top-width: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0;" />
</ui:VisualElement> </ui:VisualElement>
<!-- 검색 입력 필드 -->
<ui:TextField name="search-field" placeholder-text="검색어를 입력하세요." class="search-field"> <ui:TextField name="search-field" placeholder-text="검색어를 입력하세요." class="search-field">
<!-- 검색어 지우기 버튼 (X 아이콘, 입력 필드 오른쪽) -->
<ui:Button name="clear-btn" style="width: 18px; height: 18px; border-top-width: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0; background-color: rgba(255, 255, 255, 0); background-image: resource(&apos;SHI/Images/btn_cancel_64&apos;); margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; align-self: center; position: absolute; right: 26px; -unity-background-image-tint-color: rgb(180, 180, 180);" /> <ui:Button name="clear-btn" style="width: 18px; height: 18px; border-top-width: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0; background-color: rgba(255, 255, 255, 0); background-image: resource(&apos;SHI/Images/btn_cancel_64&apos;); margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; align-self: center; position: absolute; right: 26px; -unity-background-image-tint-color: rgb(180, 180, 180);" />
<!-- 검색 아이콘 (돋보기, 입력 필드 맨 오른쪽) -->
<ui:VisualElement style="flex-grow: 1; position: absolute; right: 6px; width: 16px; height: 17px; justify-content: center; align-items: auto; align-self: center; background-image: resource(&apos;SHI/Images/icon_search_16x17&apos;);" /> <ui:VisualElement style="flex-grow: 1; position: absolute; right: 6px; width: 16px; height: 17px; justify-content: center; align-items: auto; align-self: center; background-image: resource(&apos;SHI/Images/icon_search_16x17&apos;);" />
</ui:TextField> </ui:TextField>
<ui:TreeView name="main-tree-view" view-data-key="model-tree-view" fixed-item-height="32" auto-expand="true" item-template="project://database/Assets/Resources/SHI/Modal/TreeListItem.uxml?fileID=9197481963319205126&amp;guid=c5b3acae6c4669946b6e6b8b36d82e88&amp;type=3#TreeListItem" horizontal-scrolling="true" style="flex-grow: 1;" />
<!-- 메인 트리뷰: 모델 계층 구조를 표시
- view-data-key: 상태 저장용 키
- fixed-item-height: 항목 높이 32px
- auto-expand: 자동 펼치기 활성화 -->
<ui:TreeView name="main-tree-view" view-data-key="model-tree-view" fixed-item-height="32" auto-expand="true" style="flex-grow: 1;" />
</ui:VisualElement> </ui:VisualElement>
</ui:UXML> </ui:UXML>

View File

@@ -2,6 +2,7 @@
using Cysharp.Threading.Tasks; using Cysharp.Threading.Tasks;
using SHI.Modal.ISOP; using SHI.Modal.ISOP;
using SHI.Modal.NW; using SHI.Modal.NW;
using System.Collections.Generic;
using System.IO; using System.IO;
using UnityEngine; using UnityEngine;
using UnityEngine.UI; using UnityEngine.UI;
@@ -80,12 +81,18 @@ public class ShiPopupSample : MonoBehaviour
} }
string sa = Application.streamingAssetsPath; string sa = Application.streamingAssetsPath;
string glbPath = Path.Combine(sa, "block.glb"); string glbPath = Path.Combine(sa, "B16VC.glb");
string jsonPath = Path.Combine(sa, "isop_chart_short.json"); string jsonPath = Path.Combine(sa, "isop_chart_short.json");
Debug.Log($"Loaded blockDetailModal:{isopModal}"); Debug.Log($"Loaded blockDetailModal:{isopModal}");
await isopModal.LoadData(glbPath, jsonPath); await isopModal.LoadData(new List<string> {
Path.Combine(sa, "B11TC.glb"),
Path.Combine(sa, "B16VC.glb"),
Path.Combine(sa, "E11VC.glb"),
Path.Combine(sa, "E41UC.glb"),
Path.Combine(sa, "M11UC.glb"),
}, jsonPath);
} }
private async UniTaskVoid SetupDataNW() private async UniTaskVoid SetupDataNW()
@@ -96,12 +103,17 @@ public class ShiPopupSample : MonoBehaviour
return; return;
} }
string sa = Application.streamingAssetsPath; string sa = Application.streamingAssetsPath;
string glbPath = Path.Combine(sa, "block.glb");
string jsonPath = Path.Combine(sa, "nw_chart.json"); string jsonPath = Path.Combine(sa, "nw_chart.json");
Debug.Log($"Loaded blockDetailModal:{nwModal}"); Debug.Log($"Loaded blockDetailModal:{nwModal}");
await nwModal.LoadData(glbPath, jsonPath); await nwModal.LoadData(new List<string> {
Path.Combine(sa, "B11TC.glb"),
Path.Combine(sa, "B16VC.glb"),
Path.Combine(sa, "E11VC.glb"),
Path.Combine(sa, "E41UC.glb"),
Path.Combine(sa, "M11UC.glb"),
}, jsonPath);
} }
} }

View File

@@ -42,8 +42,12 @@ namespace SHI.Modal.ISOP
/// </list> /// </list>
/// </summary> /// </summary>
[UxmlElement] [UxmlElement]
public partial class ISOPChart : VisualElement public partial class ISOPChart : VisualElement, IDisposable
{ {
#region IDisposable
private bool _disposed = false;
#endregion
#region (Constants) #region (Constants)
/// <summary>메인 UXML 파일 경로 (Resources 폴더 기준)</summary> /// <summary>메인 UXML 파일 경로 (Resources 폴더 기준)</summary>
private const string UXML_PATH = "SHI/Modal/ISOP/ISOPChart"; private const string UXML_PATH = "SHI/Modal/ISOP/ISOPChart";
@@ -525,5 +529,47 @@ namespace SHI.Modal.ISOP
return 0; return 0;
} }
#endregion #endregion
#region IDisposable
/// <summary>
/// 리소스를 해제하고 이벤트 핸들러를 정리합니다.
/// </summary>
public void Dispose()
{
if (_disposed) return;
_disposed = true;
// 스크롤 이벤트 해제
if (contentScroll != null)
{
contentScroll.horizontalScroller.valueChanged -= OnHorizontalScroll;
}
// 외부 이벤트 정리
OnExpand = null;
// 동적으로 생성된 UI 요소 정리
monthsLayer?.Clear();
weeksLayer?.Clear();
daysLayer?.Clear();
timelineContent?.Clear();
// 데이터 정리
tasks?.Clear();
tasks = null;
// UI 참조 정리
_expandBtn = null;
taskRowTemplate = null;
root = null;
headerTitle = null;
headerTimeline = null;
monthsLayer = null;
weeksLayer = null;
daysLayer = null;
timelineContent = null;
contentScroll = null;
}
#endregion
} }
} }

View File

@@ -48,15 +48,15 @@ namespace SHI.Modal.ISOP
[SerializeField] [SerializeField]
public UIDocument uiDocument; public UIDocument uiDocument;
private VisualElement content; private VisualElement? content;
private TreeList listView; private TreeList? listView;
private ISOPModelView modelView; private ISOPModelView? modelView;
private ISOPChart chartView; private ISOPChart? chartView;
private Button closeBtn; private Button? closeBtn;
private Button showTreeBtn; private Button? showTreeBtn;
private Button dragBtn; private Button? dragBtn;
private CancellationTokenSource? _cts; private CancellationTokenSource? _cts;
private bool _suppressSelection = false; private bool _suppressSelection = false;
@@ -79,6 +79,14 @@ namespace SHI.Modal.ISOP
private float _lastModelFlexGrow = 1f; private float _lastModelFlexGrow = 1f;
private float _lastChartFlexGrow = 1f; private float _lastChartFlexGrow = 1f;
// GeometryChangedEvent 콜백 (해제용)
private EventCallback<GeometryChangedEvent>? _contentGeometryChangedCallback;
// 로딩 UI
private VisualElement? _loadingOverlay;
private VisualElement? _loadingSpinner;
private IVisualElementScheduledItem? _spinnerAnimation;
private void OnEnable() private void OnEnable()
{ {
@@ -140,8 +148,12 @@ namespace SHI.Modal.ISOP
} }
initDrag(root); initDrag(root);
_expanded = ExpandedSide.None; _expanded = ExpandedSide.None;
// 로딩 UI 참조
_loadingOverlay = root.Q<VisualElement>("loading-overlay");
_loadingSpinner = root.Q<VisualElement>("loading-spinner");
} }
private void initDrag(VisualElement root) private void initDrag(VisualElement root)
@@ -158,103 +170,107 @@ namespace SHI.Modal.ISOP
//dragBtn.AddManipulator(new HorizontalDragManipulator()); //dragBtn.AddManipulator(new HorizontalDragManipulator());
// 드래그 시작 // 드래그 이벤트 등록
dragBtn.RegisterCallback<PointerDownEvent>((evt) => dragBtn.RegisterCallback<PointerDownEvent>(OnDragPointerDown, TrickleDown.TrickleDown);
{ dragBtn.RegisterCallback<PointerMoveEvent>(OnDragPointerMove, TrickleDown.TrickleDown);
// 좌클릭만 처리 (0) dragBtn.RegisterCallback<PointerUpEvent>(OnDragPointerUp, TrickleDown.TrickleDown);
if (evt.button != 0) return; dragBtn.RegisterCallback<PointerCancelEvent>(OnDragPointerCancel, TrickleDown.TrickleDown);
Debug.Log("Drag Started (PointerDown) - captured");
// 포인터 캡처
_isDragging = true;
_activePointerId = evt.pointerId;
dragBtn.CapturePointer(_activePointerId);
// 포인터가 drag 버튼의 어느 위치를 눌렀는지 계산 (center 기준)
// evt.position은 content 기준 좌표
var dragCenterX = dragBtn.layout.x + dragBtn.layout.width * 0.5f;
_dragOffset = evt.position.x - dragCenterX;
evt.StopImmediatePropagation();
}, TrickleDown.TrickleDown);
// 전역 포인터 무브로 위치 추적 (root에 등록)
dragBtn.RegisterCallback<PointerMoveEvent>((evt) =>
{
if (!_isDragging) return;
if (evt.pointerId != _activePointerId) return;
//Debug.Log($"Dragging... evt.pointerId:{evt.pointerId}, _activePointerId:{_activePointerId}");
// evt.position은 content 기준 좌표
float pointerX = evt.position.x;
float centerX = pointerX - _dragOffset;
ApplyDragPosition(content, dragBtn, centerX);
evt.StopImmediatePropagation();
}, TrickleDown.TrickleDown);
// 드래그 종료 (마우스 업 또는 포인터 캔슬)
dragBtn.RegisterCallback<PointerUpEvent>((evt) =>
{
if (!_isDragging) return;
if (evt.pointerId != _activePointerId) return;
Debug.Log("Drag Ended");
_isDragging = false;
if (_activePointerId != -1)
{
try { dragBtn.ReleasePointer(_activePointerId); } catch { }
}
_activePointerId = -1;
evt.StopImmediatePropagation();
}, TrickleDown.TrickleDown);
dragBtn.RegisterCallback<PointerCancelEvent>((evt) =>
{
if (!_isDragging) return;
if (evt.pointerId != _activePointerId) return;
_isDragging = false;
if (_activePointerId != -1)
{
try { dragBtn.ReleasePointer(_activePointerId); } catch { }
}
_activePointerId = -1;
evt.StopImmediatePropagation();
}, TrickleDown.TrickleDown);
// 초기화 및 레이아웃 변경 시 재계산 // 초기화 및 레이아웃 변경 시 재계산
bool initialized = false; _contentGeometryChangedCallback = OnContentGeometryChanged;
content.RegisterCallback<GeometryChangedEvent>((evt) => content.RegisterCallback(_contentGeometryChangedCallback);
{
// 드래그 중에는 GeometryChanged 이벤트 무시
if (_isDragging) return;
// 초기화: treeList의 레이아웃이 계산될 때까지 대기
if (!initialized)
{
if (listView == null || listView.layout.width <= 0)
{
return; // 아직 레이아웃이 계산되지 않음
}
initialized = true;
}
UpdateDragAndPanels(content, dragBtn);
});
} }
} }
} }
private void OnDragPointerDown(PointerDownEvent evt)
{
// 좌클릭만 처리 (0)
if (evt.button != 0) return;
Debug.Log("Drag Started (PointerDown) - captured");
// 포인터 캡처
_isDragging = true;
_activePointerId = evt.pointerId;
dragBtn.CapturePointer(_activePointerId);
// 포인터가 drag 버튼의 어느 위치를 눌렀는지 계산 (center 기준)
// evt.position은 content 기준 좌표
var dragCenterX = dragBtn.layout.x + dragBtn.layout.width * 0.5f;
_dragOffset = evt.position.x - dragCenterX;
evt.StopImmediatePropagation();
}
private void OnDragPointerMove(PointerMoveEvent evt)
{
if (!_isDragging) return;
if (evt.pointerId != _activePointerId) return;
// evt.position은 content 기준 좌표
float pointerX = evt.position.x;
float centerX = pointerX - _dragOffset;
ApplyDragPosition(content, dragBtn, centerX);
evt.StopImmediatePropagation();
}
private void OnDragPointerUp(PointerUpEvent evt)
{
if (!_isDragging) return;
if (evt.pointerId != _activePointerId) return;
Debug.Log("Drag Ended");
_isDragging = false;
if (_activePointerId != -1)
{
try { dragBtn.ReleasePointer(_activePointerId); } catch { }
}
_activePointerId = -1;
evt.StopImmediatePropagation();
}
private void OnDragPointerCancel(PointerCancelEvent evt)
{
if (!_isDragging) return;
if (evt.pointerId != _activePointerId) return;
_isDragging = false;
if (_activePointerId != -1)
{
try { dragBtn.ReleasePointer(_activePointerId); } catch { }
}
_activePointerId = -1;
evt.StopImmediatePropagation();
}
private bool _geometryInitialized = false;
private void OnContentGeometryChanged(GeometryChangedEvent evt)
{
// 드래그 중에는 GeometryChanged 이벤트 무시
if (_isDragging) return;
// 초기화: treeList의 레이아웃이 계산될 때까지 대기
if (!_geometryInitialized)
{
if (listView == null || listView.layout.width <= 0)
{
return; // 아직 레이아웃이 계산되지 않음
}
_geometryInitialized = true;
}
UpdateDragAndPanels(content, dragBtn);
}
/// <summary> /// <summary>
/// glTF 모델과 간트 데이터를 하위 뷰에 로드하고, 선택 동기화를 위한 매핑을 구축합니다. /// glTF 모델과 간트 데이터를 하위 뷰에 로드하고, 선택 동기화를 위한 매핑을 구축합니다.
/// </summary> /// </summary>
/// <param name="gltfPath">glTF/glb 파일 경로.</param> /// <param name="gltfPath">glTF/glb 파일 경로.</param>
/// <param name="ganttPath">간트 데이터셋 경로.</param> /// <param name="ganttPath">간트 데이터셋 경로.</param>
/// <param name="externalCt">외부 취소 토큰.</param> /// <param name="externalCt">외부 취소 토큰.</param>
public async UniTask LoadData(string gltfPath, string ganttPath, CancellationToken externalCt = default) public async UniTask LoadData(List<string> gltfPaths, string ganttPath, CancellationToken externalCt = default)
{ {
if(modelView == null) if(modelView == null)
{ {
@@ -263,38 +279,103 @@ namespace SHI.Modal.ISOP
{ {
await UniTask.Yield(); await UniTask.Yield();
} }
} }
Debug.Log($"ISOPModal: LoadData {gltfPath}"); Debug.Log($"ISOPModal: LoadData {string.Join(", ", gltfPaths)}, {ganttPath}");
// 이전 작업 취소 // 로딩 표시
if (_cts != null) ShowLoading(true);
{
try { _cts.Cancel(); } catch { }
_cts.Dispose();
}
_cts = CancellationTokenSource.CreateLinkedTokenSource(externalCt);
var ct = _cts.Token;
// 모델/리스트 로드 try
IEnumerable<TreeListItemData> items = Array.Empty<TreeListItemData>();
if (modelView != null)
{ {
try // 이전 작업 취소
if (_cts != null)
{ {
items = await modelView.LoadModelAsync(gltfPath, ct); try { _cts.Cancel(); } catch { }
_cts.Dispose();
} }
catch (OperationCanceledException) { } _cts = CancellationTokenSource.CreateLinkedTokenSource(externalCt);
var ct = _cts.Token;
// 모델/리스트 로드
IEnumerable<TreeListItemData> items = Array.Empty<TreeListItemData>();
if (modelView != null)
{
try
{
items = await modelView.LoadModelAsync(gltfPaths, ct);
}
catch (OperationCanceledException) { }
}
BuildKeyMaps(items);
if (listView != null) listView.SetData(items.ToList());
if (chartView != null) chartView.Load(ganttPath);
}
finally
{
// 로딩 숨기기
ShowLoading(false);
}
}
#region UI (Loading UI)
/// <summary>
/// 로딩 오버레이와 스피너 애니메이션을 표시하거나 숨깁니다.
/// </summary>
private void ShowLoading(bool show)
{
if (_loadingOverlay == null) return;
if (show)
{
_loadingOverlay.AddToClassList("visible");
StartSpinnerAnimation();
}
else
{
_loadingOverlay.RemoveFromClassList("visible");
StopSpinnerAnimation();
}
}
/// <summary>
/// 스피너 회전 애니메이션 시작
/// </summary>
private void StartSpinnerAnimation()
{
if (_loadingSpinner == null) return;
StopSpinnerAnimation();
float angle = 0f;
_spinnerAnimation = _loadingSpinner.schedule.Execute(() =>
{
angle = (angle + 10f) % 360f;
_loadingSpinner.style.rotate = new Rotate(Angle.Degrees(angle));
}).Every(16); // ~60fps
}
/// <summary>
/// 스피너 회전 애니메이션 중지
/// </summary>
private void StopSpinnerAnimation()
{
if (_spinnerAnimation != null)
{
_spinnerAnimation.Pause();
_spinnerAnimation = null;
} }
BuildKeyMaps(items); if (_loadingSpinner != null)
{
if (listView != null) listView.SetData(items.ToList()); _loadingSpinner.style.rotate = new Rotate(Angle.Degrees(0));
if (chartView != null) chartView.Load(ganttPath); }
} }
#endregion
private void BuildKeyMaps(IEnumerable<TreeListItemData> items) private void BuildKeyMaps(IEnumerable<TreeListItemData> items)
{ {
@@ -595,13 +676,24 @@ namespace SHI.Modal.ISOP
private void OnDestroy() private void OnDestroy()
{ {
// CancellationTokenSource 정리
if (_cts != null)
{
try { _cts.Cancel(); } catch { }
_cts.Dispose();
_cts = null;
}
// listView 이벤트 해제 및 Dispose
if (listView != null) if (listView != null)
{ {
listView.OnSelectionChanged -= OnListItemSelectionChanged; listView.OnSelectionChanged -= OnListItemSelectionChanged;
listView.OnClosed -= OnListClosed; listView.OnClosed -= OnListClosed;
listView.OnVisibilityChanged -= OnListVisibilityChanged; listView.OnVisibilityChanged -= OnListVisibilityChanged;
listView.Dispose();
} }
// modelView 이벤트 해제 및 Dispose
if (modelView != null) if (modelView != null)
{ {
modelView.OnItemSelected -= OnModelItemSelected; modelView.OnItemSelected -= OnModelItemSelected;
@@ -609,13 +701,56 @@ namespace SHI.Modal.ISOP
modelView.Dispose(); modelView.Dispose();
} }
// chartView 이벤트 해제 및 Dispose
if (chartView != null) if (chartView != null)
{ {
chartView.OnExpand -= ToggleExpandChart; chartView.OnExpand -= ToggleExpandChart;
chartView.Dispose();
} }
// 버튼 이벤트 해제
if (showTreeBtn != null) showTreeBtn.clicked -= OnClickShowTree; if (showTreeBtn != null) showTreeBtn.clicked -= OnClickShowTree;
if (closeBtn != null) closeBtn.clicked -= OnClickClose; if (closeBtn != null) closeBtn.clicked -= OnClickClose;
// dragBtn 포인터 이벤트 해제
if (dragBtn != null)
{
dragBtn.UnregisterCallback<PointerDownEvent>(OnDragPointerDown, TrickleDown.TrickleDown);
dragBtn.UnregisterCallback<PointerMoveEvent>(OnDragPointerMove, TrickleDown.TrickleDown);
dragBtn.UnregisterCallback<PointerUpEvent>(OnDragPointerUp, TrickleDown.TrickleDown);
dragBtn.UnregisterCallback<PointerCancelEvent>(OnDragPointerCancel, TrickleDown.TrickleDown);
}
// content GeometryChangedEvent 해제
if (content != null && _contentGeometryChangedCallback != null)
{
content.UnregisterCallback(_contentGeometryChangedCallback);
_contentGeometryChangedCallback = null;
}
// 딕셔너리 정리
_keyToId.Clear();
_idToKey.Clear();
// 드래그 상태 초기화
_isDragging = false;
_activePointerId = -1;
_dragOffset = 0f;
_geometryInitialized = false;
// 로딩 UI 정리
StopSpinnerAnimation();
_loadingOverlay = null;
_loadingSpinner = null;
// UI 참조 정리
content = null;
listView = null;
modelView = null;
chartView = null;
closeBtn = null;
showTreeBtn = null;
dragBtn = null;
} }
} }

View File

@@ -46,8 +46,12 @@ namespace SHI.Modal.ISOP
/// </list> /// </list>
/// </summary> /// </summary>
[UxmlElement] [UxmlElement]
public partial class ISOPModelView : VisualElement public partial class ISOPModelView : VisualElement, IDisposable
{ {
#region IDisposable
private bool _disposed = false;
#endregion
#region (Public Events) #region (Public Events)
/// <summary> /// <summary>
/// 뷰 내부에서 항목이 선택될 때 발생합니다. /// 뷰 내부에서 항목이 선택될 때 발생합니다.
@@ -156,35 +160,55 @@ namespace SHI.Modal.ISOP
} }
/// <summary> /// <summary>
/// 주어진 경로의 glTF 모델을 비동기로 로드하고, UI 트리에 사용할 계층 항목을 생성합니다. /// 주어진 경로의 glTF 모델을 비동기로 로드하고, UI 트리에 사용할 계층 항목을 생성합니다.
/// </summary> /// </summary>
public async UniTask<IEnumerable<TreeListItemData>> LoadModelAsync(string path, CancellationToken ct) /// <param name="paths">로드할 glTF/glb 파일 경로 목록</param>
/// <param name="ct">취소 토큰</param>
/// <returns>로드된 모델들의 계층 항목 목록</returns>
public async UniTask<IEnumerable<TreeListItemData>> LoadModelAsync(List<string> paths, CancellationToken ct)
{ {
Debug.Log($"ISOPModelView.LoadModelAsync: {path}"); Debug.Log($"ISOPModelView.LoadModelAsync: {paths?.Count ?? 0} files");
Dispose();
CleanupForReload();
await UniTask.DelayFrame(1); await UniTask.DelayFrame(1);
EnsureCameraAndTargetTexture(); EnsureCameraAndTargetTexture();
var items = new List<TreeListItemData>(); var items = new List<TreeListItemData>();
var gltf = new GltfImport();
var success = await gltf.Load(path, new ImportSettings(), ct); if (paths == null || paths.Count == 0)
if (!success)
{ {
Debug.LogError($"glTFast Load failed: {path}"); Debug.LogWarning("ISOPModelView.LoadModelAsync: No paths provided");
return items; return items;
} }
if(_root == null) _root = new GameObject("ISOPModelViewRoot"); if (_root == null) _root = new GameObject("ISOPModelViewRoot");
_root.layer = modelLayer; _root.layer = modelLayer;
var sceneOk = await gltf.InstantiateMainSceneAsync(_root.transform);
if (!sceneOk) // 각 파일을 순차적으로 로드
foreach (var path in paths)
{ {
Debug.LogError("InstantiateMainSceneAsync failed"); if (string.IsNullOrEmpty(path)) continue;
return items;
Debug.Log($"ISOPModelView.LoadModelAsync: Loading {path}");
var gltf = new GltfImport();
var success = await gltf.Load(path, new ImportSettings(), ct);
if (!success)
{
Debug.LogError($"glTFast Load failed: {path}");
continue;
}
var sceneOk = await gltf.InstantiateMainSceneAsync(_root.transform);
if (!sceneOk)
{
Debug.LogError($"InstantiateMainSceneAsync failed: {path}");
continue;
}
} }
SetLayerRecursive(_root, modelLayer); SetLayerRecursive(_root, modelLayer);
// 로드된 모든 자식에서 TreeListItemData 생성
if (_root != null) if (_root != null)
{ {
for (int i = 0; i < _root.transform.childCount; i++) for (int i = 0; i < _root.transform.childCount; i++)
@@ -600,8 +624,13 @@ namespace SHI.Modal.ISOP
} }
} }
public void Dispose() /// <summary>
/// 모델 재로드를 위한 리소스 정리.
/// Dispose()와 달리 UI 참조(_renderContainer, _expandBtn)는 유지합니다.
/// </summary>
private void CleanupForReload()
{ {
// 머티리얼 정리 (인스턴스 머티리얼 삭제)
foreach (var kv in _originalSharedByRenderer) foreach (var kv in _originalSharedByRenderer)
{ {
var r = kv.Key; var r = kv.Key;
@@ -616,11 +645,14 @@ namespace SHI.Modal.ISOP
} }
_originalSharedByRenderer.Clear(); _originalSharedByRenderer.Clear();
_idToObject.Clear(); _idToObject.Clear();
// 모델 루트 삭제
if (_root != null) UnityEngine.Object.Destroy(_root); if (_root != null) UnityEngine.Object.Destroy(_root);
_root = null; _root = null;
_focusedId = null; _focusedId = null;
_wireframeApplied = false; _wireframeApplied = false;
// 카메라 및 렌더텍스처 정리
if (_viewCamera != null) if (_viewCamera != null)
{ {
if (_rt != null && _viewCamera.targetTexture == _rt) if (_rt != null && _viewCamera.targetTexture == _rt)
@@ -629,6 +661,7 @@ namespace SHI.Modal.ISOP
} }
_viewCamera.enabled = false; _viewCamera.enabled = false;
} }
if (_rt != null) if (_rt != null)
{ {
_rt.Release(); _rt.Release();
@@ -636,13 +669,95 @@ namespace SHI.Modal.ISOP
_rt = null; _rt = null;
} }
//ISOPModelViewRig란 이름의 게임오브젝트도 같이 삭제 // 드래그 상태 초기화
_mmbDragging = false;
_rmbDragging = false;
// ID 시드 초기화
itemIdSeed = 1;
// 게임오브젝트 정리
var rigGo = GameObject.Find("ISOPModelViewRig");
if (rigGo != null) UnityEngine.Object.Destroy(rigGo);
_viewCamera = null;
var rootGo = GameObject.Find("ISOPModelViewRoot");
if (rootGo != null) UnityEngine.Object.Destroy(rootGo);
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
// 마우스 이벤트 해제
UnregisterCallback<MouseDownEvent>(OnMouseDown);
UnregisterCallback<MouseUpEvent>(OnMouseUp);
UnregisterCallback<MouseMoveEvent>(OnMouseMove);
UnregisterCallback<WheelEvent>(OnWheel);
UnregisterCallback<GeometryChangedEvent>(OnGeometryChanged);
// 외부 이벤트 정리
OnItemSelected = null;
OnExpand = null;
// 머티리얼 정리 (인스턴스 머티리얼 삭제)
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;
}
_originalSharedByRenderer.Clear();
_idToObject.Clear();
// 모델 루트 삭제
if (_root != null) UnityEngine.Object.Destroy(_root);
_root = null;
_focusedId = null;
_wireframeApplied = false;
_wireframeMat = null; // Resources에서 로드한 것은 Destroy하지 않음
// 카메라 및 렌더텍스처 정리
if (_viewCamera != null)
{
if (_rt != null && _viewCamera.targetTexture == _rt)
{
_viewCamera.targetTexture = null;
}
_viewCamera.enabled = false;
}
_viewCamera = null;
if (_rt != null)
{
_rt.Release();
UnityEngine.Object.Destroy(_rt);
_rt = null;
}
// 드래그 상태 초기화
_mmbDragging = false;
_rmbDragging = false;
// ID 시드 초기화
itemIdSeed = 1;
// UI 참조 정리
_renderContainer = null;
_expandBtn = null;
// 게임오브젝트 정리
var rigGo = GameObject.Find("ISOPModelViewRig"); var rigGo = GameObject.Find("ISOPModelViewRig");
if (rigGo != null) UnityEngine.Object.Destroy(rigGo); if (rigGo != null) UnityEngine.Object.Destroy(rigGo);
var rootGo = GameObject.Find("ISOPModelViewRoot"); var rootGo = GameObject.Find("ISOPModelViewRoot");
if (rootGo != null) UnityEngine.Object.Destroy(rootGo); if (rootGo != null) UnityEngine.Object.Destroy(rootGo);
} }
// 유틸리티 메서드 // 유틸리티 메서드

View File

@@ -64,8 +64,12 @@ namespace SHI.Modal.NW
/// </list> /// </list>
/// </summary> /// </summary>
[UxmlElement] [UxmlElement]
public partial class NWChart : VisualElement public partial class NWChart : VisualElement, IDisposable
{ {
#region IDisposable
private bool _disposed = false;
#endregion
private const string UXML_PATH = "SHI/Modal/NW/NWChart"; private const string UXML_PATH = "SHI/Modal/NW/NWChart";
public Action? OnExpand; public Action? OnExpand;
@@ -123,7 +127,9 @@ namespace SHI.Modal.NW
private bool isDragging; private bool isDragging;
private DateTime projectStartDate; private DateTime projectStartDate;
public DateTime ProjectStartDate => projectStartDate;
private DateTime projectEndDate; private DateTime projectEndDate;
public DateTime ProjectEndDate => projectEndDate;
private int totalDays; private int totalDays;
private float canvasHeight; private float canvasHeight;
@@ -142,10 +148,15 @@ namespace SHI.Modal.NW
_expandBtn = this.Q<Button>("expand-btn"); _expandBtn = this.Q<Button>("expand-btn");
if (_expandBtn != null) if (_expandBtn != null)
{ {
_expandBtn.clicked += () => OnExpand?.Invoke(); _expandBtn.clicked += OnExpandBtnClicked;
} }
} }
private void OnExpandBtnClicked()
{
OnExpand?.Invoke();
}
public void Load(string jsonFileName) public void Load(string jsonFileName)
{ {
root = this; root = this;
@@ -157,6 +168,45 @@ namespace SHI.Modal.NW
RenderNetwork(); RenderNetwork();
} }
/// <summary>
/// 지정한 날짜가 STDT~FNDT 범위에 포함되는 작업들의 BLK_NO 목록을 반환합니다.
/// </summary>
/// <param name="yyyymmdd">조회할 날짜 (yyyyMMdd 형식, 예: "20250818")</param>
/// <returns>해당 날짜에 진행 중인 블록 번호 목록</returns>
public List<string> GetModelNamesByDate(string yyyymmdd)
{
var result = new List<string>();
if (tasks == null || string.IsNullOrEmpty(yyyymmdd)) return result;
foreach (var task in tasks)
{
// BLK_NO가 없으면 스킵
if (string.IsNullOrEmpty(task.BLK_NO)) continue;
// STDT, FNDT 가져오기
string stdt = task.STDT;
string fndt = task.FNDT;
// STDT가 없으면 스킵
if (string.IsNullOrEmpty(stdt) || stdt == "null") continue;
// FNDT가 없으면 STDT와 동일하게 처리
if (string.IsNullOrEmpty(fndt) || fndt == "null") fndt = stdt;
// 문자열 비교로 범위 체크 (yyyyMMdd 형식은 문자열 비교로 날짜 비교 가능)
if (string.CompareOrdinal(stdt, yyyymmdd) <= 0 && string.CompareOrdinal(yyyymmdd, fndt) <= 0)
{
// 중복 방지
if (!result.Contains(task.BLK_NO))
{
result.Add(task.BLK_NO);
}
}
}
return result;
}
void LoadData(string jsonFileName) void LoadData(string jsonFileName)
{ {
var json = File.ReadAllText(jsonFileName); var json = File.ReadAllText(jsonFileName);
@@ -428,7 +478,7 @@ namespace SHI.Modal.NW
var task = tasks!.Find( (item) => item.STDT == date.ToString("yyyyMMdd")); var task = tasks!.Find( (item) => item.STDT == date.ToString("yyyyMMdd"));
if(task != null) if(task != null)
{ {
Debug.Log($"Found task for date {date:yyyyMMdd}: REL_TP={task.REL_TP}, PROJ_TP={task.PROJ_TP}"); // Debug.Log($"Found task for date {date:yyyyMMdd}: REL_TP={task.REL_TP}, PROJ_TP={task.PROJ_TP}");
var lab = new Label($"{task.REL_TP}\n{task.PROJ_TP}"); var lab = new Label($"{task.REL_TP}\n{task.PROJ_TP}");
lab.style.unityTextAlign = TextAnchor.MiddleRight; lab.style.unityTextAlign = TextAnchor.MiddleRight;
lab.style.width = Length.Percent(100); lab.style.width = Length.Percent(100);
@@ -940,5 +990,69 @@ namespace SHI.Modal.NW
if (draggingActivityCode == null) return; if (draggingActivityCode == null) return;
UpdateConnectionsForNode(draggingActivityCode); UpdateConnectionsForNode(draggingActivityCode);
} }
#region IDisposable
/// <summary>
/// 리소스를 해제하고 이벤트 핸들러를 정리합니다.
/// </summary>
public void Dispose()
{
if (_disposed) return;
_disposed = true;
// 버튼 이벤트 해제
if (_expandBtn != null)
{
_expandBtn.clicked -= OnExpandBtnClicked;
}
// 스크롤 이벤트 해제
if (contentScroll != null)
{
contentScroll.horizontalScroller.valueChanged -= OnHorizontalScroll;
}
// 노드 드래그 이벤트 해제
foreach (var node in nodeElements.Values)
{
node.UnregisterCallback<PointerDownEvent>(OnNodePointerDown);
node.UnregisterCallback<PointerMoveEvent>(OnNodePointerMove);
node.UnregisterCallback<PointerUpEvent>(OnNodePointerUp);
}
// 외부 이벤트 정리
OnExpand = null;
// 동적으로 생성된 UI 요소 정리
monthsLayer?.Clear();
daysLayer?.Clear();
nodesLayer?.Clear();
linesLayer?.Clear();
// 데이터 정리
tasks?.Clear();
tasks = null;
tasksByActivityCode.Clear();
nodeElements.Clear();
nodePositions.Clear();
connectionCache.Clear();
// 드래그 상태 정리
draggingNode = null;
draggingActivityCode = null;
isDragging = false;
// UI 참조 정리
_expandBtn = null;
root = null;
headerTimeline = null;
monthsLayer = null;
daysLayer = null;
networkCanvas = null;
nodesLayer = null;
linesLayer = null;
contentScroll = null;
}
#endregion
} }
} }

View File

@@ -50,13 +50,14 @@ namespace SHI.Modal.NW
[SerializeField] [SerializeField]
public UIDocument uiDocument; public UIDocument uiDocument;
private TreeList listView; private TreeList? listView;
private NWModelView modelView; private NWModelView? modelView;
private NWChart chartView; private NWChart? chartView;
private PlayBar? _playBar;
private Button closeBtn; private Button? closeBtn;
private Button showTreeBtn; private Button? showTreeBtn;
private Button dragBtn; private Button? dragBtn;
private VisualElement? rightPanel; private VisualElement? rightPanel;
@@ -81,6 +82,13 @@ namespace SHI.Modal.NW
private float _lastModelFlexGrow = 1f; private float _lastModelFlexGrow = 1f;
private float _lastChartFlexGrow = 1f; private float _lastChartFlexGrow = 1f;
// GeometryChangedEvent 콜백 (해제용)
private EventCallback<GeometryChangedEvent>? _rightPanelGeometryChangedCallback;
// 로딩 UI
private VisualElement? _loadingOverlay;
private VisualElement? _loadingSpinner;
private IVisualElementScheduledItem? _spinnerAnimation;
private void OnEnable() private void OnEnable()
{ {
@@ -141,11 +149,45 @@ namespace SHI.Modal.NW
closeBtn.clicked += OnClickClose; closeBtn.clicked += OnClickClose;
} }
_playBar = root.Q<PlayBar>("play-bar");
if (_playBar != null)
{
_playBar.SetTimeRange(DateTime.Now, DateTime.Now.AddHours(1));
_playBar.OnPlayProgress += OnPlayProgressHandler;
_playBar.OnPositionChanged += OnPlayPositionChangedHandler;
}
initDrag(root); initDrag(root);
_expanded = ExpandedSide.None; _expanded = ExpandedSide.None;
// 로딩 UI 참조
_loadingOverlay = root.Q<VisualElement>("loading-overlay");
_loadingSpinner = root.Q<VisualElement>("loading-spinner");
} }
private void OnPlayProgressHandler(DateTime time)
{
if(chartView != null && listView != null)
{
List<string> models = chartView.GetModelNamesByDate(time.ToString("yyyyMMdd"));
Debug.Log($"Models at {time:yyyyMMdd}: {string.Join(", ", models)}");
if(models.Count > 0) listView.ShowItems(models);
}
}
private void OnPlayPositionChangedHandler(DateTime time)
{
if(chartView != null && listView != null)
{
List<string> models = chartView.GetModelNamesByDate(time.ToString("yyyyMMdd"));
Debug.Log($"Models at {time:yyyyMMdd}: {string.Join(", ", models)}");
if(models.Count > 0) listView.ShowItems(models);
}
}
private void initDrag(VisualElement root) private void initDrag(VisualElement root)
{ {
dragBtn = root.Q<Button>("drag-btn"); dragBtn = root.Q<Button>("drag-btn");
@@ -157,100 +199,114 @@ namespace SHI.Modal.NW
dragBtn.style.position = Position.Absolute; dragBtn.style.position = Position.Absolute;
dragBtn.pickingMode = PickingMode.Position; dragBtn.pickingMode = PickingMode.Position;
// 드래그 시작 // 드래그 이벤트 등록
dragBtn.RegisterCallback<PointerDownEvent>((evt) => dragBtn.RegisterCallback<PointerDownEvent>(OnDragPointerDown, TrickleDown.TrickleDown);
{ dragBtn.RegisterCallback<PointerMoveEvent>(OnDragPointerMove, TrickleDown.TrickleDown);
// 좌클릭만 처리 (0) dragBtn.RegisterCallback<PointerUpEvent>(OnDragPointerUp, TrickleDown.TrickleDown);
if (evt.button != 0) return; dragBtn.RegisterCallback<PointerCancelEvent>(OnDragPointerCancel, TrickleDown.TrickleDown);
Debug.Log("Drag Started (PointerDown) - captured");
// 포인터 캡처
_isDragging = true;
_activePointerId = evt.pointerId;
dragBtn.CapturePointer(_activePointerId);
// 포인터가 drag 버튼의 어느 위치를 눌렀는지 계산 (center 기준, Y축)
var dragCenterY = dragBtn.layout.y + dragBtn.layout.height * 0.5f;
_dragOffset = evt.position.y - dragCenterY;
evt.StopImmediatePropagation();
}, TrickleDown.TrickleDown);
// 전역 포인터 무브로 위치 추적
dragBtn.RegisterCallback<PointerMoveEvent>((evt) =>
{
if (!_isDragging) return;
if (evt.pointerId != _activePointerId) return;
// evt.position은 rightPanel 기준 좌표
float pointerY = evt.position.y;
float centerY = pointerY - _dragOffset;
ApplyDragPosition(rightPanel, dragBtn, centerY);
evt.StopImmediatePropagation();
}, TrickleDown.TrickleDown);
// 드래그 종료 (마우스 업 또는 포인터 캔슬)
dragBtn.RegisterCallback<PointerUpEvent>((evt) =>
{
if (!_isDragging) return;
if (evt.pointerId != _activePointerId) return;
Debug.Log("Drag Ended");
_isDragging = false;
if (_activePointerId != -1)
{
try { dragBtn.ReleasePointer(_activePointerId); } catch { }
}
_activePointerId = -1;
evt.StopImmediatePropagation();
}, TrickleDown.TrickleDown);
dragBtn.RegisterCallback<PointerCancelEvent>((evt) =>
{
if (!_isDragging) return;
if (evt.pointerId != _activePointerId) return;
_isDragging = false;
if (_activePointerId != -1)
{
try { dragBtn.ReleasePointer(_activePointerId); } catch { }
}
_activePointerId = -1;
evt.StopImmediatePropagation();
}, TrickleDown.TrickleDown);
// 초기화 및 레이아웃 변경 시 재계산 // 초기화 및 레이아웃 변경 시 재계산
bool initialized = false; _rightPanelGeometryChangedCallback = OnRightPanelGeometryChanged;
rightPanel.RegisterCallback<GeometryChangedEvent>((evt) => rightPanel.RegisterCallback(_rightPanelGeometryChangedCallback);
{
// 드래그 중에는 GeometryChanged 이벤트 무시
if (_isDragging) return;
// 초기화: rightPanel의 레이아웃이 계산될 때까지 대기
if (!initialized)
{
if (rightPanel.layout.height <= 0)
{
return; // 아직 레이아웃이 계산되지 않음
}
initialized = true;
}
UpdateDragAndPanels(rightPanel, dragBtn);
});
} }
} }
} }
private void OnDragPointerDown(PointerDownEvent evt)
{
// 좌클릭만 처리 (0)
if (evt.button != 0) return;
Debug.Log("Drag Started (PointerDown) - captured");
// 포인터 캡처
_isDragging = true;
_activePointerId = evt.pointerId;
dragBtn?.CapturePointer(_activePointerId);
// 포인터가 drag 버튼의 어느 위치를 눌렀는지 계산 (center 기준, Y축)
if (dragBtn != null)
{
var dragCenterY = dragBtn.layout.y + dragBtn.layout.height * 0.5f;
_dragOffset = evt.position.y - dragCenterY;
}
evt.StopImmediatePropagation();
}
private void OnDragPointerMove(PointerMoveEvent evt)
{
if (!_isDragging) return;
if (evt.pointerId != _activePointerId) return;
// evt.position은 rightPanel 기준 좌표
float pointerY = evt.position.y;
float centerY = pointerY - _dragOffset;
if (rightPanel != null && dragBtn != null)
{
ApplyDragPosition(rightPanel, dragBtn, centerY);
}
evt.StopImmediatePropagation();
}
private void OnDragPointerUp(PointerUpEvent evt)
{
if (!_isDragging) return;
if (evt.pointerId != _activePointerId) return;
Debug.Log("Drag Ended");
_isDragging = false;
if (_activePointerId != -1)
{
try { dragBtn?.ReleasePointer(_activePointerId); } catch { }
}
_activePointerId = -1;
evt.StopImmediatePropagation();
}
private void OnDragPointerCancel(PointerCancelEvent evt)
{
if (!_isDragging) return;
if (evt.pointerId != _activePointerId) return;
_isDragging = false;
if (_activePointerId != -1)
{
try { dragBtn?.ReleasePointer(_activePointerId); } catch { }
}
_activePointerId = -1;
evt.StopImmediatePropagation();
}
private bool _geometryInitialized = false;
private void OnRightPanelGeometryChanged(GeometryChangedEvent evt)
{
// 드래그 중에는 GeometryChanged 이벤트 무시
if (_isDragging) return;
// 초기화: rightPanel의 레이아웃이 계산될 때까지 대기
if (!_geometryInitialized)
{
if (rightPanel == null || rightPanel.layout.height <= 0)
{
return; // 아직 레이아웃이 계산되지 않음
}
_geometryInitialized = true;
}
if (rightPanel != null && dragBtn != null)
{
UpdateDragAndPanels(rightPanel, dragBtn);
}
}
/// <summary> /// <summary>
/// glTF 모델과 간트 데이터를 하위 뷰에 로드하고, 선택 동기화를 위한 매핑을 구축합니다. /// glTF 모델과 간트 데이터를 하위 뷰에 로드하고, 선택 동기화를 위한 매핑을 구축합니다.
/// </summary> /// </summary>
/// <param name="gltfPath">glTF/glb 파일 경로.</param> /// <param name="gltfPaths">glTF/glb 파일 경로 목록.</param>
/// <param name="ganttPath">간트 데이터셋 경로.</param> /// <param name="ganttPath">간트 데이터셋 경로.</param>
/// <param name="externalCt">외부 취소 토큰.</param> /// <param name="externalCt">외부 취소 토큰.</param>
public async UniTask LoadData(string gltfPath, string ganttPath, CancellationToken externalCt = default) public async UniTask LoadData(List<string> gltfPaths, string ganttPath, CancellationToken externalCt = default)
{ {
if(modelView == null) if(modelView == null)
{ {
@@ -259,38 +315,107 @@ namespace SHI.Modal.NW
{ {
await UniTask.Yield(); await UniTask.Yield();
} }
} }
Debug.Log($"NWModal: LoadData {gltfPath}"); Debug.Log($"NWModal.LoadData: gltfPath:{string.Join(", ", gltfPaths)}, ganttPath={ganttPath}");
// 이전 작업 취소 // 로딩 표시
if (_cts != null) ShowLoading(true);
{
try { _cts.Cancel(); } catch { }
_cts.Dispose();
}
_cts = CancellationTokenSource.CreateLinkedTokenSource(externalCt);
var ct = _cts.Token;
// 모델/리스트 로드 try
IEnumerable<TreeListItemData> items = Array.Empty<TreeListItemData>();
if (modelView != null)
{ {
try // 이전 작업 취소
if (_cts != null)
{ {
items = await modelView.LoadModelAsync(gltfPath, ct); try { _cts.Cancel(); } catch { }
_cts.Dispose();
} }
catch (OperationCanceledException) { } _cts = CancellationTokenSource.CreateLinkedTokenSource(externalCt);
var ct = _cts.Token;
// 모델/리스트 로드
IEnumerable<TreeListItemData> items = Array.Empty<TreeListItemData>();
if (modelView != null)
{
try
{
items = await modelView.LoadModelAsync(gltfPaths, ct);
}
catch (OperationCanceledException) { }
}
BuildKeyMaps(items);
if (listView != null) listView.SetData(items.ToList());
if (chartView != null) chartView.Load(ganttPath);
if (_playBar != null && chartView != null)
{
_playBar.SetTimeRange(chartView.ProjectStartDate, chartView.ProjectEndDate);
}
}
finally
{
// 로딩 숨기기
ShowLoading(false);
}
}
#region UI (Loading UI)
/// <summary>
/// 로딩 오버레이와 스피너 애니메이션을 표시하거나 숨깁니다.
/// </summary>
private void ShowLoading(bool show)
{
if (_loadingOverlay == null) return;
if (show)
{
_loadingOverlay.AddToClassList("visible");
StartSpinnerAnimation();
}
else
{
_loadingOverlay.RemoveFromClassList("visible");
StopSpinnerAnimation();
}
}
/// <summary>
/// 스피너 회전 애니메이션 시작
/// </summary>
private void StartSpinnerAnimation()
{
if (_loadingSpinner == null) return;
StopSpinnerAnimation();
float angle = 0f;
_spinnerAnimation = _loadingSpinner.schedule.Execute(() =>
{
angle = (angle + 10f) % 360f;
_loadingSpinner.style.rotate = new Rotate(Angle.Degrees(angle));
}).Every(16); // ~60fps
}
/// <summary>
/// 스피너 회전 애니메이션 중지
/// </summary>
private void StopSpinnerAnimation()
{
if (_spinnerAnimation != null)
{
_spinnerAnimation.Pause();
_spinnerAnimation = null;
} }
BuildKeyMaps(items); if (_loadingSpinner != null)
{
if (listView != null) listView.SetData(items.ToList()); _loadingSpinner.style.rotate = new Rotate(Angle.Degrees(0));
if (chartView != null) chartView.Load(ganttPath); }
} }
#endregion
private void BuildKeyMaps(IEnumerable<TreeListItemData> items) private void BuildKeyMaps(IEnumerable<TreeListItemData> items)
{ {
@@ -566,13 +691,24 @@ namespace SHI.Modal.NW
private void OnDestroy() private void OnDestroy()
{ {
// CancellationTokenSource 정리
if (_cts != null)
{
try { _cts.Cancel(); } catch { }
_cts.Dispose();
_cts = null;
}
// listView 이벤트 해제 및 Dispose
if (listView != null) if (listView != null)
{ {
listView.OnSelectionChanged -= OnListItemSelectionChanged; listView.OnSelectionChanged -= OnListItemSelectionChanged;
listView.OnClosed -= OnListClosed; listView.OnClosed -= OnListClosed;
listView.OnVisibilityChanged -= OnListVisibilityChanged; listView.OnVisibilityChanged -= OnListVisibilityChanged;
listView.Dispose();
} }
// modelView 이벤트 해제 및 Dispose
if (modelView != null) if (modelView != null)
{ {
modelView.OnItemSelected -= OnModelItemSelected; modelView.OnItemSelected -= OnModelItemSelected;
@@ -580,13 +716,63 @@ namespace SHI.Modal.NW
modelView.Dispose(); modelView.Dispose();
} }
// chartView 이벤트 해제 및 Dispose
if (chartView != null) if (chartView != null)
{ {
chartView.OnExpand -= ToggleExpandChart; chartView.OnExpand -= ToggleExpandChart;
chartView.Dispose();
} }
// 버튼 이벤트 해제
if (showTreeBtn != null) showTreeBtn.clicked -= OnClickShowTree; if (showTreeBtn != null) showTreeBtn.clicked -= OnClickShowTree;
if (closeBtn != null) closeBtn.clicked -= OnClickClose; if (closeBtn != null) closeBtn.clicked -= OnClickClose;
// dragBtn 포인터 이벤트 해제
if (dragBtn != null)
{
dragBtn.UnregisterCallback<PointerDownEvent>(OnDragPointerDown, TrickleDown.TrickleDown);
dragBtn.UnregisterCallback<PointerMoveEvent>(OnDragPointerMove, TrickleDown.TrickleDown);
dragBtn.UnregisterCallback<PointerUpEvent>(OnDragPointerUp, TrickleDown.TrickleDown);
dragBtn.UnregisterCallback<PointerCancelEvent>(OnDragPointerCancel, TrickleDown.TrickleDown);
}
// rightPanel GeometryChangedEvent 해제
if (rightPanel != null && _rightPanelGeometryChangedCallback != null)
{
rightPanel.UnregisterCallback(_rightPanelGeometryChangedCallback);
_rightPanelGeometryChangedCallback = null;
}
if( _playBar != null)
{
_playBar.OnPlayProgress -= OnPlayProgressHandler;
_playBar.OnPositionChanged -= OnPlayPositionChangedHandler;
_playBar.Dispose();
}
// 딕셔너리 정리
_keyToId.Clear();
_idToKey.Clear();
// 드래그 상태 초기화
_isDragging = false;
_activePointerId = -1;
_dragOffset = 0f;
_geometryInitialized = false;
// 로딩 UI 정리
StopSpinnerAnimation();
_loadingOverlay = null;
_loadingSpinner = null;
// UI 참조 정리
rightPanel = null;
listView = null;
modelView = null;
chartView = null;
closeBtn = null;
showTreeBtn = null;
dragBtn = null;
} }
} }

View File

@@ -46,8 +46,12 @@ namespace SHI.Modal.NW
/// </list> /// </list>
/// </summary> /// </summary>
[UxmlElement] [UxmlElement]
public partial class NWModelView : VisualElement public partial class NWModelView : VisualElement, IDisposable
{ {
#region IDisposable
private bool _disposed = false;
#endregion
#region (Public Events) #region (Public Events)
/// <summary> /// <summary>
/// 뷰 내부에서 항목이 선택될 때 발생합니다. /// 뷰 내부에서 항목이 선택될 때 발생합니다.
@@ -141,10 +145,7 @@ namespace SHI.Modal.NW
_expandBtn = this.Q<Button>("expand-btn"); _expandBtn = this.Q<Button>("expand-btn");
if(_expandBtn != null) if(_expandBtn != null)
{ {
_expandBtn.clicked += () => _expandBtn.clicked += OnExpandBtnClicked;
{
OnExpand?.Invoke();
};
} }
// 마우스 이벤트 등록 // 마우스 이벤트 등록
@@ -155,36 +156,60 @@ namespace SHI.Modal.NW
RegisterCallback<GeometryChangedEvent>(OnGeometryChanged); RegisterCallback<GeometryChangedEvent>(OnGeometryChanged);
} }
/// <summary> private void OnExpandBtnClicked()
/// 주어진 경로의 glTF 모델을 비동기로 로드하고, UI 트리에 사용할 계층 항목을 생성합니다.
/// </summary>
public async UniTask<IEnumerable<TreeListItemData>> LoadModelAsync(string path, CancellationToken ct)
{ {
Debug.Log($"NWModelView.LoadModelAsync: {path}"); OnExpand?.Invoke();
Dispose(); }
/// <summary>
/// 주어진 경로들의 glTF 모델을 비동기로 로드하고, UI 트리에 사용할 계층 항목을 생성합니다.
/// </summary>
/// <param name="paths">로드할 glTF/glb 파일 경로 목록</param>
/// <param name="ct">취소 토큰</param>
/// <returns>로드된 모델들의 계층 항목 목록</returns>
public async UniTask<IEnumerable<TreeListItemData>> LoadModelAsync(List<string> paths, CancellationToken ct)
{
Debug.Log($"NWModelView.LoadModelAsync: {paths?.Count ?? 0} files");
CleanupForReload();
await UniTask.DelayFrame(1); await UniTask.DelayFrame(1);
EnsureCameraAndTargetTexture(); EnsureCameraAndTargetTexture();
var items = new List<TreeListItemData>(); var items = new List<TreeListItemData>();
var gltf = new GltfImport();
var success = await gltf.Load(path, new ImportSettings(), ct); if (paths == null || paths.Count == 0)
if (!success)
{ {
Debug.LogError($"glTFast Load failed: {path}"); Debug.LogWarning("NWModelView.LoadModelAsync: No paths provided");
return items; return items;
} }
if(_root == null) _root = new GameObject("NWModelViewRoot"); if (_root == null) _root = new GameObject("NWModelViewRoot");
_root.layer = modelLayer; _root.layer = modelLayer;
var sceneOk = await gltf.InstantiateMainSceneAsync(_root.transform);
if (!sceneOk) // 각 파일을 순차적으로 로드
foreach (var path in paths)
{ {
Debug.LogError("InstantiateMainSceneAsync failed"); if (string.IsNullOrEmpty(path)) continue;
return items;
Debug.Log($"NWModelView.LoadModelAsync: Loading {path}");
var gltf = new GltfImport();
var success = await gltf.Load(path, new ImportSettings(), ct);
if (!success)
{
Debug.LogError($"glTFast Load failed: {path}");
continue;
}
var sceneOk = await gltf.InstantiateMainSceneAsync(_root.transform);
if (!sceneOk)
{
Debug.LogError($"InstantiateMainSceneAsync failed: {path}");
continue;
}
} }
SetLayerRecursive(_root, modelLayer); SetLayerRecursive(_root, modelLayer);
// 로드된 모든 자식에서 TreeListItemData 생성
if (_root != null) if (_root != null)
{ {
for (int i = 0; i < _root.transform.childCount; i++) for (int i = 0; i < _root.transform.childCount; i++)
@@ -313,7 +338,11 @@ namespace SHI.Modal.NW
private void EnsureRenderTargetSize() private void EnsureRenderTargetSize()
{ {
if (_viewCamera == null || _renderContainer == null) return; if (_viewCamera == null || _renderContainer == null)
{
Debug.LogWarning($"[NWModelView] EnsureRenderTargetSize: camera={_viewCamera != null}, container={_renderContainer != null}");
return;
}
// 고정 크기 사용 // 고정 크기 사용
int w = FIXED_RT_WIDTH; int w = FIXED_RT_WIDTH;
@@ -377,12 +406,10 @@ namespace SHI.Modal.NW
public void FocusItemById(int id) public void FocusItemById(int id)
{ {
Debug.Log($"NWModelView.FocusItemById: id={id}");
_focusedId = id; _focusedId = id;
if (_idToObject.TryGetValue(id, out var go)) if (_idToObject.TryGetValue(id, out var go))
{ {
Highlight(go, true); Highlight(go, true);
Debug.Log($"NWModelView.FocusItemById: {go.name}");
_orbitTarget = go.transform.position; _orbitTarget = go.transform.position;
} }
} }
@@ -398,17 +425,14 @@ namespace SHI.Modal.NW
public void Export(int id) public void Export(int id)
{ {
Debug.Log($"NWModelView.Export: id={id}");
if (_idToObject.TryGetValue(id, out var go)) if (_idToObject.TryGetValue(id, out var go))
{ {
Debug.Log($"Exporting object: {go.name}");
UVC.GLTF.GLTFExporter.ExportNodeByExplorer(go); UVC.GLTF.GLTFExporter.ExportNodeByExplorer(go);
} }
} }
public void SetVisibility(int id, bool on) public void SetVisibility(int id, bool on)
{ {
Debug.Log($"NWModelView.SetVisibility: id={id} on={on}");
if (_idToObject.TryGetValue(id, out var go)) if (_idToObject.TryGetValue(id, out var go))
{ {
go.SetActive(on); go.SetActive(on);
@@ -601,8 +625,12 @@ namespace SHI.Modal.NW
} }
} }
public void Dispose() /// <summary>
/// 모델 재로드를 위한 정리 (UI 참조는 유지)
/// </summary>
private void CleanupForReload()
{ {
// 머티리얼 정리 (인스턴스 머티리얼 삭제)
foreach (var kv in _originalSharedByRenderer) foreach (var kv in _originalSharedByRenderer)
{ {
var r = kv.Key; var r = kv.Key;
@@ -617,11 +645,14 @@ namespace SHI.Modal.NW
} }
_originalSharedByRenderer.Clear(); _originalSharedByRenderer.Clear();
_idToObject.Clear(); _idToObject.Clear();
// 모델 루트 삭제
if (_root != null) UnityEngine.Object.Destroy(_root); if (_root != null) UnityEngine.Object.Destroy(_root);
_root = null; _root = null;
_focusedId = null; _focusedId = null;
_wireframeApplied = false; _wireframeApplied = false;
// 카메라 및 렌더텍스처 정리
if (_viewCamera != null) if (_viewCamera != null)
{ {
if (_rt != null && _viewCamera.targetTexture == _rt) if (_rt != null && _viewCamera.targetTexture == _rt)
@@ -630,6 +661,7 @@ namespace SHI.Modal.NW
} }
_viewCamera.enabled = false; _viewCamera.enabled = false;
} }
if (_rt != null) if (_rt != null)
{ {
_rt.Release(); _rt.Release();
@@ -637,13 +669,101 @@ namespace SHI.Modal.NW
_rt = null; _rt = null;
} }
//NWModelViewRig란 이름의 게임오브젝트도 같이 삭제 // 드래그 상태 초기화
_mmbDragging = false;
_rmbDragging = false;
// ID 시드 초기화
itemIdSeed = 1;
// 게임오브젝트 정리
var rigGo = GameObject.Find("NWModelViewRig");
if (rigGo != null) UnityEngine.Object.Destroy(rigGo);
_viewCamera = null;
var rootGo = GameObject.Find("NWModelViewRoot");
if (rootGo != null) UnityEngine.Object.Destroy(rootGo);
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
// 버튼 이벤트 해제
if (_expandBtn != null)
{
_expandBtn.clicked -= OnExpandBtnClicked;
}
// 마우스 이벤트 해제
UnregisterCallback<MouseDownEvent>(OnMouseDown);
UnregisterCallback<MouseUpEvent>(OnMouseUp);
UnregisterCallback<MouseMoveEvent>(OnMouseMove);
UnregisterCallback<WheelEvent>(OnWheel);
UnregisterCallback<GeometryChangedEvent>(OnGeometryChanged);
// 외부 이벤트 정리
OnItemSelected = null;
OnExpand = null;
// 머티리얼 정리 (인스턴스 머티리얼 삭제)
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;
}
_originalSharedByRenderer.Clear();
_idToObject.Clear();
// 모델 루트 삭제
if (_root != null) UnityEngine.Object.Destroy(_root);
_root = null;
_focusedId = null;
_wireframeApplied = false;
_wireframeMat = null; // Resources에서 로드한 것은 Destroy하지 않음
// 카메라 및 렌더텍스처 정리
if (_viewCamera != null)
{
if (_rt != null && _viewCamera.targetTexture == _rt)
{
_viewCamera.targetTexture = null;
}
_viewCamera.enabled = false;
}
_viewCamera = null;
if (_rt != null)
{
_rt.Release();
UnityEngine.Object.Destroy(_rt);
_rt = null;
}
// 드래그 상태 초기화
_mmbDragging = false;
_rmbDragging = false;
// ID 시드 초기화
itemIdSeed = 1;
// UI 참조 정리
_renderContainer = null;
_expandBtn = null;
// 게임오브젝트 정리
var rigGo = GameObject.Find("NWModelViewRig"); var rigGo = GameObject.Find("NWModelViewRig");
if (rigGo != null) UnityEngine.Object.Destroy(rigGo); if (rigGo != null) UnityEngine.Object.Destroy(rigGo);
var rootGo = GameObject.Find("NWModelViewRoot"); var rootGo = GameObject.Find("NWModelViewRoot");
if (rootGo != null) UnityEngine.Object.Destroy(rootGo); if (rootGo != null) UnityEngine.Object.Destroy(rootGo);
} }
// 유틸리티 메서드 // 유틸리티 메서드

View File

@@ -0,0 +1,472 @@
#nullable enable
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
namespace SHI.Modal
{
/// <summary>
/// 시간 기반 재생을 제어하는 UI Toolkit 컴포넌트입니다.
/// </summary>
[UxmlElement]
public partial class PlayBar : VisualElement, IDisposable
{
#region (Constants)
private const string UXML_PATH = "SHI/Modal/PlayBar";
private const int DEFAULT_INTERVAL_SECONDS = 2;
#endregion
#region UI
private Label? _startLabel;
private Label? _endLabel;
private Label? _currentTimeLabel;
private VisualElement? _progressTrack;
private VisualElement? _progressFill;
private Button? _playBtn;
private Button? _firstBtn;
private Button? _lastBtn;
private Button? _stopBtn;
private DropdownField? _intervalDropdown;
#endregion
#region (State)
private DateTime _startTime;
private DateTime _endTime;
private DateTime _currentTime;
private bool _isPlaying;
private int _intervalSeconds = DEFAULT_INTERVAL_SECONDS;
private IVisualElementScheduledItem? _playSchedule;
private ProgressDragManipulator? _dragManipulator;
#endregion
#region (Public Events)
/// <summary>재생이 시작될 때 발생</summary>
public event Action? OnPlayStarted;
/// <summary>재생이 정지될 때 발생</summary>
public event Action? OnPlayStopped;
/// <summary>재생 중 시간이 변경될 때 발생 (자동 재생 시)</summary>
public event Action<DateTime>? OnPlayProgress;
/// <summary>사용자가 진행바를 드래그/클릭하여 위치를 변경할 때 발생</summary>
public event Action<DateTime>? OnPositionChanged;
#endregion
#region UxmlAttribute
[UxmlAttribute]
public bool IsVisible
{
get => style.display == DisplayStyle.Flex;
set => style.display = value ? DisplayStyle.Flex : DisplayStyle.None;
}
#endregion
#region (Constructor)
public PlayBar()
{
var visualTree = Resources.Load<VisualTreeAsset>(UXML_PATH);
Debug.Log("[PlayBar] UXML loaded and cloned." + (visualTree == null));
if (visualTree == null)
{
Debug.LogError($"[PlayBar] UXML not found at: {UXML_PATH}");
return;
}
visualTree.CloneTree(this);
InitializeUIReferences();
InitializeEventHandlers();
// 패널에 연결된 후 드롭다운 초기화
RegisterCallback<AttachToPanelEvent>(OnAttachToPanel);
}
private void OnAttachToPanel(AttachToPanelEvent evt)
{
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanel);
InitializeDropdown();
}
#endregion
#region (Initialization)
private void InitializeUIReferences()
{
_startLabel = this.Q<Label>("start-label");
_endLabel = this.Q<Label>("end-label");
_currentTimeLabel = this.Q<Label>("current-time-label");
_progressTrack = this.Q<VisualElement>("progress-track");
_progressFill = this.Q<VisualElement>("progress-fill");
_playBtn = this.Q<Button>("play-btn");
_firstBtn = this.Q<Button>("first-btn");
_lastBtn = this.Q<Button>("last-btn");
_stopBtn = this.Q<Button>("stop-btn");
_intervalDropdown = this.Q<DropdownField>("interval-dropdown");
}
private void InitializeEventHandlers()
{
_playBtn?.RegisterCallback<ClickEvent>(OnPlayClicked);
_firstBtn?.RegisterCallback<ClickEvent>(OnFirstClicked);
_lastBtn?.RegisterCallback<ClickEvent>(OnLastClicked);
_stopBtn?.RegisterCallback<ClickEvent>(OnStopClicked);
// 진행바 드래그/클릭 설정
if (_progressTrack != null)
{
_dragManipulator = new ProgressDragManipulator(this);
_progressTrack.AddManipulator(_dragManipulator);
}
}
private void InitializeDropdown()
{
if (_intervalDropdown == null) return;
var choices = new List<string>();
for (int i = 0; i <= 10; i++)
{
choices.Add($"{i}초");
}
_intervalDropdown.choices = choices;
_intervalDropdown.value = $"{DEFAULT_INTERVAL_SECONDS}초";
_intervalDropdown.RegisterValueChangedCallback(OnIntervalChanged);
}
#endregion
#region (Public Methods)
/// <summary>
/// 시간 범위를 설정합니다.
/// </summary>
public void SetTimeRange(DateTime start, DateTime end)
{
_startTime = start;
_endTime = end;
_currentTime = start;
UpdateLabels();
UpdateProgressBar();
}
/// <summary>
/// 현재 시간을 설정합니다.
/// </summary>
public void SetCurrentTime(DateTime time)
{
_currentTime = ClampTime(time);
UpdateLabels();
UpdateProgressBar();
}
/// <summary>
/// 간격 선택 옵션을 설정합니다.
/// </summary>
public void SetIntervalChoices(List<int> seconds)
{
if (_intervalDropdown == null) return;
var choices = new List<string>();
foreach (var sec in seconds)
{
choices.Add($"{sec}초");
}
_intervalDropdown.choices = choices;
_intervalDropdown.index = 0;
_intervalSeconds = seconds.Count > 0 ? seconds[0] : DEFAULT_INTERVAL_SECONDS;
}
/// <summary>
/// 재생을 시작합니다.
/// </summary>
public void Play()
{
if (_isPlaying) return;
_isPlaying = true;
UpdatePlayButtonState();
StartPlayTimer();
OnPlayStarted?.Invoke();
}
/// <summary>
/// 재생을 일시정지합니다.
/// </summary>
public void Pause()
{
if (!_isPlaying) return;
_isPlaying = false;
StopPlayTimer();
UpdatePlayButtonState();
}
/// <summary>
/// 재생을 정지하고 처음으로 돌아갑니다.
/// </summary>
public void Stop()
{
_isPlaying = false;
_currentTime = _startTime;
StopPlayTimer();
UpdatePlayButtonState();
UpdateLabels();
UpdateProgressBar();
OnPlayStopped?.Invoke();
}
/// <summary>
/// 리소스를 정리합니다.
/// </summary>
public void Dispose()
{
StopPlayTimer();
UnregisterEventHandlers();
OnPlayStarted = null;
OnPlayStopped = null;
OnPlayProgress = null;
OnPositionChanged = null;
}
#endregion
#region (Event Handlers)
private void OnPlayClicked(ClickEvent evt)
{
if (_isPlaying)
Pause();
else
Play();
}
private void OnFirstClicked(ClickEvent evt)
{
SetCurrentTime(_startTime);
OnPositionChanged?.Invoke(_currentTime);
}
private void OnLastClicked(ClickEvent evt)
{
SetCurrentTime(_endTime);
OnPositionChanged?.Invoke(_currentTime);
}
private void OnStopClicked(ClickEvent evt)
{
Stop();
}
private void OnIntervalChanged(ChangeEvent<string> evt)
{
// "2초" -> 2 파싱
if (int.TryParse(evt.newValue.Replace("초", ""), out int seconds))
{
_intervalSeconds = seconds;
// 재생 중이면 타이머 재시작
if (_isPlaying)
{
StopPlayTimer();
StartPlayTimer();
}
}
}
#endregion
#region (Timer)
private void StartPlayTimer()
{
_playSchedule = schedule.Execute(OnPlayTick).Every(_intervalSeconds * 1000);
}
private void StopPlayTimer()
{
_playSchedule?.Pause();
_playSchedule = null;
}
private void OnPlayTick()
{
if (!_isPlaying) return;
_currentTime = _currentTime.AddDays(1);
if (_currentTime >= _endTime)
{
_currentTime = _endTime;
Pause();
}
UpdateLabels();
UpdateProgressBar();
OnPlayProgress?.Invoke(_currentTime);
}
#endregion
#region UI (UI Updates)
private void UpdateLabels()
{
if (_startLabel != null)
_startLabel.text = _startTime.ToString("yyyy-MM-dd");
if (_endLabel != null)
_endLabel.text = _endTime.ToString("yyyy-MM-dd");
if (_currentTimeLabel != null)
_currentTimeLabel.text = _currentTime.ToString("yyyy-MM-dd HH:mm");
}
private void UpdateProgressBar()
{
if (_progressFill == null) return;
float progress = CalculateProgress();
_progressFill.style.width = Length.Percent(progress * 100);
}
private void UpdatePlayButtonState()
{
if (_playBtn == null) return;
if (_isPlaying)
_playBtn.AddToClassList("playing");
else
_playBtn.RemoveFromClassList("playing");
}
#endregion
#region (Utilities)
private float CalculateProgress()
{
double total = (_endTime - _startTime).TotalDays;
if (total <= 0) return 0;
double current = (_currentTime - _startTime).TotalDays;
return Mathf.Clamp01((float)(current / total));
}
private DateTime ClampTime(DateTime time)
{
if (time < _startTime) return _startTime;
if (time > _endTime) return _endTime;
return time;
}
/// <summary>
/// 정규화된 위치(0~1)로부터 시간을 설정합니다. (드래그용)
/// </summary>
internal void SetProgressFromPosition(float normalizedPosition)
{
double totalDays = (_endTime - _startTime).TotalDays;
double targetDays = totalDays * normalizedPosition;
_currentTime = _startTime.AddDays(targetDays);
_currentTime = ClampTime(_currentTime);
UpdateLabels();
UpdateProgressBar();
}
/// <summary>
/// 드래그 완료 시 이벤트를 발생시킵니다.
/// </summary>
internal void NotifyPositionChanged()
{
OnPositionChanged?.Invoke(_currentTime);
}
internal float GetTrackWidth()
{
return _progressTrack?.layout.width ?? 0;
}
#endregion
#region (Unregister)
private void UnregisterEventHandlers()
{
_playBtn?.UnregisterCallback<ClickEvent>(OnPlayClicked);
_firstBtn?.UnregisterCallback<ClickEvent>(OnFirstClicked);
_lastBtn?.UnregisterCallback<ClickEvent>(OnLastClicked);
_stopBtn?.UnregisterCallback<ClickEvent>(OnStopClicked);
if (_progressTrack != null && _dragManipulator != null)
{
_progressTrack.RemoveManipulator(_dragManipulator);
}
}
#endregion
}
/// <summary>
/// 진행바의 드래그/클릭을 처리하는 Manipulator입니다.
/// </summary>
public class ProgressDragManipulator : PointerManipulator
{
private readonly PlayBar _playBar;
private bool _isActive;
public ProgressDragManipulator(PlayBar playBar)
{
_playBar = playBar;
activators.Add(new ManipulatorActivationFilter { button = MouseButton.LeftMouse });
}
protected override void RegisterCallbacksOnTarget()
{
target.RegisterCallback<PointerDownEvent>(OnPointerDown);
target.RegisterCallback<PointerMoveEvent>(OnPointerMove);
target.RegisterCallback<PointerUpEvent>(OnPointerUp);
target.RegisterCallback<PointerCaptureOutEvent>(OnPointerCaptureOut);
}
protected override void UnregisterCallbacksFromTarget()
{
target.UnregisterCallback<PointerDownEvent>(OnPointerDown);
target.UnregisterCallback<PointerMoveEvent>(OnPointerMove);
target.UnregisterCallback<PointerUpEvent>(OnPointerUp);
target.UnregisterCallback<PointerCaptureOutEvent>(OnPointerCaptureOut);
}
private void OnPointerDown(PointerDownEvent evt)
{
if (!CanStartManipulation(evt)) return;
_isActive = true;
target.CapturePointer(evt.pointerId);
UpdatePosition(evt.localPosition);
evt.StopPropagation();
}
private void OnPointerMove(PointerMoveEvent evt)
{
if (!_isActive || !target.HasPointerCapture(evt.pointerId)) return;
UpdatePosition(evt.localPosition);
}
private void OnPointerUp(PointerUpEvent evt)
{
if (!_isActive) return;
_isActive = false;
target.ReleasePointer(evt.pointerId);
// 드래그 종료 시에만 이벤트 발생
_playBar.NotifyPositionChanged();
evt.StopPropagation();
}
private void OnPointerCaptureOut(PointerCaptureOutEvent evt)
{
_isActive = false;
}
private void UpdatePosition(Vector2 localPosition)
{
float trackWidth = _playBar.GetTrackWidth();
if (trackWidth <= 0) return;
float normalizedPosition = Mathf.Clamp01(localPosition.x / trackWidth);
_playBar.SetProgressFromPosition(normalizedPosition);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ea6077d0bdca20a4e954f6ab8a93e349

View File

@@ -45,14 +45,15 @@ namespace SHI.Modal
/// </list> /// </list>
/// </summary> /// </summary>
[UxmlElement] [UxmlElement]
public partial class TreeList : VisualElement public partial class TreeList : VisualElement, IDisposable
{ {
#region IDisposable
private bool _disposed = false;
#endregion
#region (Constants) #region (Constants)
/// <summary>메인 UXML 파일 경로 (Resources 폴더 기준)</summary> /// <summary>메인 UXML 파일 경로 (Resources 폴더 기준)</summary>
private const string UXML_PATH = "SHI/Modal/TreeList"; private const string UXML_PATH = "SHI/Modal/TreeList";
/// <summary>개별 항목 템플릿 UXML 파일 경로 (Resources 폴더 기준)</summary>
private const string ITEM_UXML_PATH = "SHI/Modal/TreeListItem";
#endregion #endregion
#region UI (UI Component References) #region UI (UI Component References)
@@ -70,11 +71,7 @@ namespace SHI.Modal
#endregion #endregion
#region (Internal Data) #region (Internal Data)
/// <summary>
/// 개별 항목 UI를 생성할 때 사용하는 UXML 템플릿입니다.
/// 생성자에서 한 번만 로드하여 재사용합니다.
/// </summary>
private VisualTreeAsset _itemTemplate;
/// <summary> /// <summary>
/// 원본 루트 데이터입니다. /// 원본 루트 데이터입니다.
@@ -106,19 +103,19 @@ namespace SHI.Modal
/// 항목의 가시성(눈 아이콘)이 변경될 때 발생합니다. /// 항목의 가시성(눈 아이콘)이 변경될 때 발생합니다.
/// 3D 모델의 GameObject 활성화/비활성화에 연동합니다. /// 3D 모델의 GameObject 활성화/비활성화에 연동합니다.
/// </summary> /// </summary>
public event Action<TreeListItemData> OnVisibilityChanged; public event Action<TreeListItemData>? OnVisibilityChanged;
/// <summary> /// <summary>
/// 항목 선택 상태가 변경될 때 발생합니다. /// 항목 선택 상태가 변경될 때 발생합니다.
/// 선택 및 선택 해제 모두에서 발생하며, item.isSelected로 구분합니다. /// 선택 및 선택 해제 모두에서 발생하며, item.isSelected로 구분합니다.
/// </summary> /// </summary>
public event Action<TreeListItemData> OnSelectionChanged; public event Action<TreeListItemData>? OnSelectionChanged;
/// <summary> /// <summary>
/// 트리 리스트가 닫힐 때(숨겨질 때) 발생합니다. /// 트리 리스트가 닫힐 때(숨겨질 때) 발생합니다.
/// 닫기 버튼 클릭 시 트리거됩니다. /// 닫기 버튼 클릭 시 트리거됩니다.
/// </summary> /// </summary>
public event Action OnClosed; public event Action? OnClosed;
#endregion #endregion
#region (Constructor) #region (Constructor)
@@ -138,21 +135,14 @@ namespace SHI.Modal
} }
visualTree!.CloneTree(this); visualTree!.CloneTree(this);
// 2. 항목 템플릿 UXML 로드 (성능을 위해 미리 로드)
_itemTemplate = Resources.Load<VisualTreeAsset>(ITEM_UXML_PATH);
if (_itemTemplate == null)
{
Debug.LogError($"[TreeMenu] Item UXML not found at: {ITEM_UXML_PATH}");
return;
}
// 3. 자식 요소 참조 획득 (UXML의 name 속성으로 찾음) // 2. 자식 요소 참조 획득 (UXML의 name 속성으로 찾음)
_searchField = this.Q<TextField>("search-field"); _searchField = this.Q<TextField>("search-field");
_treeView = this.Q<TreeView>("main-tree-view"); _treeView = this.Q<TreeView>("main-tree-view");
_closeButton = this.Q<Button>("hide-btn"); _closeButton = this.Q<Button>("hide-btn");
_clearButton = this.Q<Button>("clear-btn"); _clearButton = this.Q<Button>("clear-btn");
// 4. 이벤트 연결 및 로직 초기화 // 3. 이벤트 연결 및 로직 초기화
InitializeLogic(); InitializeLogic();
} }
#endregion #endregion
@@ -166,7 +156,7 @@ namespace SHI.Modal
// 검색창 이벤트: 입력 값이 변경될 때마다 필터링 실행 // 검색창 이벤트: 입력 값이 변경될 때마다 필터링 실행
if (_searchField != null) if (_searchField != null)
{ {
_searchField.RegisterValueChangedCallback(evt => OnSearch(evt.newValue)); _searchField.RegisterValueChangedCallback(OnSearchValueChanged);
} }
// TreeView 설정 // TreeView 설정
@@ -175,7 +165,6 @@ namespace SHI.Modal
// selectionChanged: 선택 변경 시 호출 // selectionChanged: 선택 변경 시 호출
if (_treeView != null) if (_treeView != null)
{ {
_treeView.makeItem = MakeTreeItem;
_treeView.bindItem = BindTreeItem; _treeView.bindItem = BindTreeItem;
_treeView.selectionChanged += OnTreeViewSelectionChanged; _treeView.selectionChanged += OnTreeViewSelectionChanged;
} }
@@ -249,6 +238,59 @@ namespace SHI.Modal
{ {
_treeView.SetSelection(new List<int> { itemId }); _treeView.SetSelection(new List<int> { itemId });
} }
/// <summary>
/// 지정된 이름 목록에 해당하는 항목만 표시하고 나머지는 숨깁니다.
/// </summary>
/// <param name="items">표시할 항목들의 이름 목록</param>
/// <param name="depth">검색 깊이 (1=1뎁스 자식만, 2=2뎁스까지, 0이하=전체)</param>
public void ShowItems(List<string> items, int depth = 1)
{
if (_originalRoots == null || _originalRoots.Count == 0) return;
var visibleNames = new HashSet<string>(items ?? new List<string>());
foreach (var root in _originalRoots)
{
SetVisibilityByNames(root, visibleNames, depth, 0);
}
// UI 갱신
_treeView?.RefreshItems();
}
/// <summary>
/// 재귀적으로 항목의 가시성을 이름 목록에 따라 설정합니다.
/// </summary>
/// <param name="node">현재 노드</param>
/// <param name="visibleNames">표시할 이름 목록</param>
/// <param name="maxDepth">최대 검색 깊이 (0이하=무제한)</param>
/// <param name="currentDepth">현재 깊이</param>
private void SetVisibilityByNames(TreeListItemData node, HashSet<string> visibleNames, int maxDepth, int currentDepth)
{
if (node == null) return;
// maxDepth <= 0 이면 전체 검색, 아니면 깊이 제한 체크
bool isWithinDepth = maxDepth <= 0 || currentDepth < maxDepth;
if (isWithinDepth)
{
// 이름이 목록에 있으면 visible, 없으면 hidden
node.IsVisible = visibleNames.Contains(node.name);
// 가시성 변경 이벤트 발송
OnVisibilityChanged?.Invoke(node);
}
// 자식들도 재귀적으로 처리
if (node.children != null)
{
foreach (var child in node.children)
{
SetVisibilityByNames(child, visibleNames, maxDepth, currentDepth + 1);
}
}
}
#endregion #endregion
#region (Selection Handling) #region (Selection Handling)
@@ -320,20 +362,8 @@ namespace SHI.Modal
} }
#endregion #endregion
#region TreeView / (TreeView Item Creation/Binding) #region TreeView (TreeView Item Creation/Binding)
/// <summary>
/// TreeView의 새 행(Row) UI를 생성합니다.
/// TreeView의 makeItem 콜백으로 사용됩니다.
/// 스크롤 시 재사용되는 풀링 시스템에서 호출됩니다.
/// </summary>
/// <returns>항목 템플릿에서 생성된 VisualElement</returns>
private VisualElement MakeTreeItem()
{
// UXML 템플릿을 복제하여 새 항목 생성
var templateContainer = _itemTemplate.Instantiate();
return templateContainer;
}
/// <summary> /// <summary>
/// 데이터를 UI 요소에 바인딩합니다. /// 데이터를 UI 요소에 바인딩합니다.
/// TreeView의 bindItem 콜백으로 사용됩니다. /// TreeView의 bindItem 콜백으로 사용됩니다.
@@ -419,6 +449,15 @@ namespace SHI.Modal
#endregion #endregion
#region (Search Functionality) #region (Search Functionality)
/// <summary>
/// 검색 필드 값 변경 콜백입니다.
/// </summary>
/// <param name="evt">값 변경 이벤트</param>
private void OnSearchValueChanged(ChangeEvent<string> evt)
{
OnSearch(evt.newValue);
}
/// <summary> /// <summary>
/// 검색어에 따라 트리를 필터링합니다. /// 검색어에 따라 트리를 필터링합니다.
/// 검색어가 비어있으면 원본 데이터로 복원됩니다. /// 검색어가 비어있으면 원본 데이터로 복원됩니다.
@@ -536,5 +575,50 @@ namespace SHI.Modal
} }
} }
#endregion #endregion
#region IDisposable
/// <summary>
/// 리소스를 해제하고 이벤트 핸들러를 정리합니다.
/// </summary>
public void Dispose()
{
if (_disposed) return;
_disposed = true;
// 검색 필드 이벤트 해제
if (_searchField != null)
{
_searchField.UnregisterValueChangedCallback(OnSearchValueChanged);
}
// TreeView 이벤트 핸들러 해제
if (_treeView != null)
{
_treeView.selectionChanged -= OnTreeViewSelectionChanged;
_treeView.bindItem = null;
_treeView.makeItem = null;
}
// 외부 이벤트 구독자 정리
OnVisibilityChanged = null;
OnSelectionChanged = null;
OnClosed = null;
// 데이터 정리
_originalRoots.Clear();
_rootData?.Clear();
_rootData = null;
_previouslySelectedItem = null;
// ID 시드 초기화
_idSeed = 1;
// UI 참조 정리
_searchField = null;
_treeView = null;
_closeButton = null;
_clearButton = null;
}
#endregion
} }
} }

Binary file not shown.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: aaa870cd5572f574f9e0f35548fa2023
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 39d2ad74d28f1964a8bee00a8430137b
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: df415f0f9df02cb47b8b6a466dcaeccc
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 3d1bea67a9624cf479b1a59c4654226d
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 929c8703b7d263c4495d923b7236ce63
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1 +1,3 @@
@import url("unity-theme://default"); @import url("unity-theme://default");
@import url("/Assets/Resources/SHI/Modal/Modal.uss");