PyQt는 C++ UI 크로스 플랫폼 프레임워크인 Qt 프레임워크를 파이썬으로 작업할 수 있게 도와주는 라이브러리입니다. 해당 라이브러리에서는 GUI/XML/SQL 작업을 진행할 수 있습니다.


설치 방법


Windows

pip3 install PyQt5


OSX

brew install pyqt


참조 : https://help.ubuntu.com/community/CronHowto


Crontab은 특정 시간에 설정해놓은 커맨드를 실행하는 서비스입니다.


특정 시간에 실행할 커맨드를 등록하는 Crontab 파일을 수정할 수 있게 해주는 명령어입니다. 첫 실행 시, 파일 에디터를 선택합니다. 저장하고 마치면 자동으로 Crontab에 install해줍니다.

crontab -e


특정 시간에 root권한으로 실행할 커맨드를 등록하는 Crontab 파일을 수정할 수 있게 해주는 명령어입니다. 위와는 다른 파일이 열립니다. 마찬가지로 저장하고 마치면 자동으로 Crontab에 install해줍니다.

sudo crontab -e


위에서 등록한 Crontab파일을 제거하는 명령어입니다.

crontab -r

sudo crontab -r


크론탭 파일을 열면 아래와 같은 문서가 보입니다.

# Edit this file to introduce tasks to be run by cron.

# 

# Each task to run has to be defined through a single line

# indicating with different fields when the task will be run

# and what command to run for the task

# 

# To define the time you can provide concrete values for

# minute (m), hour (h), day of month (dom), month (mon),

# and day of week (dow) or use '*' in these fields (for 'any').# 

# Notice that tasks will be started based on the cron's system

# daemon's notion of time and timezones.

# 

# Output of the crontab jobs (including errors) is sent through

# email to the user the crontab file belongs to (unless redirected).

# 

# For example, you can run a backup of all your user accounts

# at 5 a.m every week with:

# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/

# 

# For more information see the manual pages of crontab(5) and cron(8)

# 

# m h  dom mon dow   command

해당 문서에 아래와 같은 명령어를 추가해줍니다.

분 시 일 월 요일 /usr/bin/somedirectory/somecommand

분 : 0~59

시 : 0~23 (0 : 자정)

일 : 1~31

월 : 1~12

요일 : 0~6(0 : 일요일)


아래는 예시입니다.

1월 1일 월요일 4시 1분에 /usr/bin/somedirectory/somecommand를 실행하는 Crontab line입니다.

01 04 1 1 1 /usr/bin/somedirectory/somecommand

매일 4시 1분에 /usr/bin/somedirectory/somecommand를 실행하는 Crontab line입니다.

01 04 * * * /usr/bin/somedirectory/somecommand

요일은 상관없이 1월, 6월 1일부터 15일까지 4시, 5시 1분, 31분 마다 /usr/bin/somedirectory/somecommand를 실행하는 Crontab line입니다.

01,31 04,05 1-15 1,6 * /usr/bin/somedirectory/somecommand

매 10분마다(0분, 10분, 20분, 30분, 40분, 50분) /usr/bin/somedirectory/somecommand를 실행하는 Crontab line입니다.

*/10 * * * * /usr/bin/somedirectory/somecommand


아래는 시간 위치에 사용할 수 있는 특수한 문자열입니다.

string

meaning

@reboot

컴퓨터가 실행될 때 실행합니다.

@yearly

매년 1월 1일 0시 0분에 실행합니다.(0 0 1 1 *)

@annually

@yearly와 같습니다.

@monthly

매달 1일 0시 0분에 실행합니다. (0 0 1 * *)

@weekly

매주 일요일 0시 0분에 실행합니다. (0 0 * * 0)

@daily

매일 0시 0분에 실행합니다. (0 0 * * *)

@midnight

@daily와 같습니다.

@hourly

Run once an hour, "0 * * * *".


아래와 같이 사용할 수 있습니다.

@reboot /path/to/execuable1


crontab 로그 확인(logging)

cat /var/log/syslog 를 통해 crontab의 실행 여부를 확인할 수 있습니다.

postfix를 설치(sudo apt-get install postfix)하시면 cat /var/mail/username를 통해 crontab이 남긴 log를 확인하실 수 있습니다.

아니면 아래를 통해 사용자 정의한 위치에 로그를 남기실 수 있습니다.

01 14 * * * /path/to/myscript >> /home/log/myscript.log 2>&1

14시 1분에 /path/to/myscript를 실행한 후, 로그를 /home/log/myscript.log에 저장합니다.
여기서 2>&1은 stderr를 stdout으로 변경한다는 뜻입니다.


출처 : http://answers.unity3d.com/questions/458207/copy-a-component-at-runtime.html


1
2
3
4
5
6
7
8
9
10
11
12
T CopyComponent<T>(T original, GameObject destination) where T : Component
 {
     System.Type type = original.GetType();
     Component copy = destination.AddComponent(type);
     System.Reflection.FieldInfo[] fields = type.GetFields();
     foreach (System.Reflection.FieldInfo field in fields)
     {
         field.SetValue(copy, field.GetValue(original));
     }
     return copy as T;
 }
 
cs


Type.GetFiels

(https://msdn.microsoft.com/ko-kr/library/ch9714z3(v=vs.110).aspx)

 - 현재 타입에서 public 필드를 가져오는 함수입니다. [SerializeField]는 가져오지 않습니다.


참고 : www.dyn4j.org/2010/01/sat/



분할 축 정리(Separating Axis Theorem)란?

 분할축 정리(Separating Axis Theorem, 이하 SAT)란 "두 물체를 투영한 구간이 겹치지 않는 축이 하나라도 존재한다면 두 물체는 교차하지 않은 상태이다."라는 명제에 관한 정리입니다. 그리고 이것을 통해 충돌을 검출해내는 알고리즘을 만들것입니다. 충돌 유무, 충돌 위치, 충돌 량을 검출하며 2D물리 시뮬레이션에서 일반적으로 사용하게 됩니다.


(http://chessire.tistory.com/entry/%EC%84%A0%EB%B6%84%EA%B3%BC-%EC%84%A0%EB%B6%84%EC%9D%98-%EC%B6%A9%EB%8F%8C-%EC%A0%95%EB%A6%AC)

 기본적인 원리는 선분충돌과 같습니다. 두 도형의 모든 선분의 직교선분에 각 도형을 투영하여 교차여부를 검출하는 방식입니다.




아래는 코드입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
Shape = function(count, size, rotation)
{
    var self = this;
 
    PIXI.Container.call(this);
 
    var vertices = [];
    var shape = new PIXI.Graphics();
    
    shape.beginFill(Math.random() * 0xffffff);
    shape.lineStyle(2, 0x101010, 1);
 
    var angle = 2 * Math.PI / count;
    for(var i = 0 ; i < count ; ++i)
    {
        var x = Math.cos(rotation + angle * i) * size;
        var y = Math.sin(rotation + angle * i) * size;
 
        vertices.push(new PIXI.Vector(x, y));
 
        if(i == 0)
            shape.moveTo(x, y);
        else
            shape.lineTo(x, y);
    }
    shape.lineTo(vertices[0].x, vertices[0].y);
    shape.endFill();
 
    this.addChild(shape);
 
    this.getVertexPosition = function(index)
    {
        return vertices[index].clone().add(new PIXI.Vector(this.position.x, this.position.y));
    }
 
    this.getAxes = function()
    {
        var axes = [];
        for(var i = 0 ; i < vertices.length ; ++i)
        {
            var vec = vertices[i+1 >= vertices.length ? 0 : i+1].clone().sub(vertices[i]);
            axes.push(new PIXI.Vector(vec.y, -vec.x).normalize());
        }
        
        return axes;
    }
 
    this.project = function(axis)
    {
        var posit = new PIXI.Vector(self.position.x, self.position.y);
        var min = axis.dot(vertices[0].clone().add(posit));
        var max = min;
        for(var i = 1 ; i < vertices.length ; ++i)
        {
            var p = axis.dot(vertices[i].clone().add(posit));
            if(min > p)
                min = p;
            if(max < p)
                max = p;
        }
 
        return new Projection(min, max);
    }
}
cs





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
Projection = function(min, max)
{
    this.min = min;
    this.max = max;
}
 
Projection.prototype.constructor = Projection;
Projection.prototype.isOverlap = function(proj)
{
    if (proj.min < this.max && this.min < proj.max)
        return true;
 
    return false;
}
 
Projection.prototype.getOverlap = function(proj)
{
    var left = this.getOverlapLeft(proj);
    var right = this.getOverlapRight(proj);
 
    return right - left;
}
 
Projection.prototype.getOverlapLeft = function(proj)
{
    return this.min < proj.min ? proj.min : this.min;
}
 
Projection.prototype.getOverlapRight = function(proj)
{
    return this.max > proj.max ? proj.max : this.max;
}
 
Projection.prototype.getOverlapCenter = function(proj)
{
    var left = this.getOverlapLeft(proj);
    var right = this.getOverlapRight(proj);
 
    return left + (right - left) * 0.5;
}
 
cs




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
function update()
{
    var axesA = shapeA.getAxes();
    var axesB = shapeB.getAxes();
 
    var overlap = 9999;
    var smallest = null;
 
    var overlapDistance = 9999;
    var isA = false;
 
    var isCollide = true;
 
    for(var k = 0 ; k < axesA.length ; ++k)
    {
        var projA = shapeA.project(axesA[k]);
        var projB = shapeB.project(axesA[k]);
        if(projA.isOverlap(projB) === false)
            isCollide = false;
        else
        {
            var o = projA.getOverlap(projB);
            if(overlap - 0.00001 <= o && o <= overlap + 0.00001)
            {
                var dist = Math.abs(projA.getOverlapCenter(projB) - axesA[k].dot(shapeA.getVertexPosition(k)));
 
                if(overlapDistance > dist)
                {
                    overlap = o;
                    smallest = axesA[k];
                    overlapDistance = dist;
                    isA = true;
                }                        
            }
            else if(overlap > o)
            {
                overlap = o;
                smallest = axesA[k];
                overlapDistance = Math.abs(projA.getOverlapCenter(projB) - axesA[k].dot(shapeA.getVertexPosition(k)));
                isA = true;
            }
        }
    }
 
    for(var k = 0 ; k < axesB.length ; ++k)
    {
        var projA = shapeA.project(axesB[k]);
        var projB = shapeB.project(axesB[k]);
        if(projA.isOverlap(projB) === false)
            isCollide = false;
        else
        {
            var o = projA.getOverlap(projB);
            if(overlap - 0.00001 <= o && o <= overlap + 0.00001)
            {
                var dist = Math.abs(projA.getOverlapCenter(projB) - axesB[k].dot(shapeB.getVertexPosition(k)));
 
                if(overlapDistance > dist)
                {
                    overlap = o;
                    smallest = axesB[k];
                    overlapDistance = dist;
                    isA = false;
                }
            }
            else if(overlap > o)
            {
                overlap = o;
                smallest = axesB[k];
                overlapDistance = Math.abs(projA.getOverlapCenter(projB) - axesB[k].dot(shapeB.getVertexPosition(k)));
                isA = false;
            }
        }
    }
 
    if(isCollide)
    {
        smallest.multiplyScalar(overlap);
        if (shapeA.dragging ^ isA)
            smallest.multiplyScalar(-1);
 
        if(shapeA.dragging)
        {
            shapeB.position.x += smallest.x;
            shapeB.position.y += smallest.y;
        }
        else
        {
            shapeA.position.x += smallest.x;
            shapeA.position.y += smallest.y;
        }
    }
}
cs



참고 : http://www.techjawab.com/2014/08/how-to-install-transmission-on.html



Transmission 설치

sudo apt-get update sudo apt-get install transmission-daemon



디렉토리 생성

mkdir /your/torrent/directory/inprogress mkdir /your/torrent/directory/complete

 원하는 디렉토리에 inprogress(다운로드 중) 폴더와 complete(다운로드 완료) 폴더를 생성합니다.



권한

sudo usermod -a -G pi debian-transmission

 Transmission은 debian-transmission이라는 유저로써 실행됩니다. 보안 문제 때문에 유저를 변경하는 것은 권장하지 않는다네요. 그렇기 때문에 다운로드 디렉토리에 접근을 위하여 이 유저의 설정을 변경하는 명령어를 입력합니다. 위 명령으로 debian-transmission에 pi그룹을 더해주게됩니다.



Transmission 설정 변경

sudo vim /etc/transmission-daemon/settings.json

 Transmission의 설정 파일은 /etc/transmission-daemon/settings.json 입니다.

여기서 download-dir, incomplete-dir, rpc-username, rpc-password를 수정해주시면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
{
    "alt-speed-down": 50,
    "alt-speed-enabled": false,
    "alt-speed-time-begin": 540,
    "alt-speed-time-day": 127,
    "alt-speed-time-enabled": false,
    "alt-speed-time-end": 1020,
    "alt-speed-up": 50,
    "bind-address-ipv4": "0.0.0.0",
    "bind-address-ipv6": "::",
    "blocklist-enabled": false,
    "blocklist-url": "http://www.example.com/blocklist",
    "cache-size-mb": 4,
    "dht-enabled": true,
    "download-dir": "/your/torrent/directory/complete",
    "download-limit": 100,
    "download-limit-enabled": 0,
    "download-queue-enabled": true,
    "download-queue-size": 5,
    "encryption": 1,
    "idle-seeding-limit": 30,
    "idle-seeding-limit-enabled": false,
    "incomplete-dir": "/your/torrent/directory/inprogress",
    "incomplete-dir-enabled": false,
    "lpd-enabled": false,
    "max-peers-global": 200,
    "message-level": 1,
    "peer-congestion-algorithm": "",
    "peer-id-ttl-hours": 6,
    "peer-limit-global": 200,
    "peer-limit-per-torrent": 50,
    "peer-port": 51413,
    "peer-port-random-high": 65535,
    "peer-port-random-low": 49152,
    "peer-port-random-on-start": false,
    "peer-socket-tos": "default",
    "pex-enabled": true,
    "port-forwarding-enabled": false,
    "preallocation": 1,
    "prefetch-enabled": 1,
    "queue-stalled-enabled": true,
    "queue-stalled-minutes": 30,
    "ratio-limit": 2,
    "ratio-limit-enabled": false,
    "rename-partial-files": true,
    "rpc-authentication-required": true,
    "rpc-bind-address": "0.0.0.0",
    "rpc-enabled": true,
    "rpc-password": "password",
    "rpc-port": 9091,
    "rpc-url": "/transmission/",
    "rpc-username": "username",
    "rpc-whitelist": "127.0.0.1",
    "rpc-whitelist-enabled": true,
    "scrape-paused-torrents-enabled": true,
    "script-torrent-done-enabled": false,
    "script-torrent-done-filename": "",
    "seed-queue-enabled": false,
    "seed-queue-size": 10,
    "speed-limit-down": 100,
    "speed-limit-down-enabled": false,
    "speed-limit-up": 100,
    "speed-limit-up-enabled": false,
    "start-added-torrents": true,
    "trash-original-torrent-files": false,
    "umask": 18,
    "upload-limit": 100,
    "upload-limit-enabled": 0,
    "upload-slots-per-torrent": 14,
    "utp-enabled": true
}
cs



Reload Transmission

sudo service transmission-daemon reload


접속

http://your_raspberry_pi_IP:9091

 위에서 설정한 아이디와 패스워드로 접속하시면 아래의 창을 보실 수 있을꺼에요.

403:Forbidden 에러


 다시 /etc/transmission-daemon/settings.json를 열어서 "rpc-whitelist-enabled"를 false로 변경해주시거나 "rpc-whitelist"에 접속하는 ip를 추가해주시면 됩니다.

Save

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// (1) 스크린샷용 카메라를 준비합니다.
screenShotCamera.gameObject.SetActive(true);
 
// (2) 화면 크기를 지정합니다.
Vector2 screenSize = new Vector2(Screen.width, Screen.height);
 
// (3) 저장할 이미지의 크기를 지정합니다.(화면 크기 그대로 저장을 원하면 screenSize로 대체하시면 됩니다.)
Vector2 imageSize = new Vector2(
    cat.pictureSize.x / cameraSize.x * screenSize.x,
    cat.pictureSize.y / cameraSize.y * screenSize.y);
 
// (4) 저장할 이미지의 Offset을 지정합니다.
Vector2 imageOffset = cat.pictureOffset;
imageOffset.x += cameraSize.x * 0.5f - transform.position.x;
imageOffset.y += cameraSize.y * 0.5f - transform.position.y;
 
// (5) OpenGL의 경우 y축이 Upwards이고, 나머지의 경우 y축이 Downwards입니다.
if (SystemInfo.graphicsDeviceType != UnityEngine.Rendering.GraphicsDeviceType.OpenGL2 &&
    SystemInfo.graphicsDeviceType != UnityEngine.Rendering.GraphicsDeviceType.OpenGLCore &&
    SystemInfo.graphicsDeviceType != UnityEngine.Rendering.GraphicsDeviceType.OpenGLES2 &&
    SystemInfo.graphicsDeviceType != UnityEngine.Rendering.GraphicsDeviceType.OpenGLES3)
    imageOffset.y = cameraSize.y - imageOffset.y;
 
imageOffset.x *= screenSize.x / cameraSize.x - imageSize.x * 0.5f;
imageOffset.y *= screenSize.y / cameraSize.y - imageSize.y * 0.5f;
 
// (6) RenderTexture에 screenShotCamera가 보고 있는 화면을 Render 합니다.
RenderTexture rt = new RenderTexture((int)screenSize.x, (int)screenSize.y, 32);
screenShotCamera.targetTexture = rt;
screenShotCamera.Render();
RenderTexture.active = rt;
 
// (7) RenderTexture를 Texture2D로 옮깁니다.
Texture2D cache = new Texture2D((int)imageSize.x, (int)imageSize.y, TextureFormat.ARGB32, false);
cache.filterMode = FilterMode.Bilinear;
cache.ReadPixels(new Rect(imageOffset, imageSize), 0, 0);
 
// (8) 저장합니다.
byte[] bytes = cache.EncodeToPNG();
string filename = Application.persistentDataPath + "/filename.png";
System.IO.File.WriteAllBytes(filename, bytes);
 
// (9) 뒷정리합니다.
screenShotCamera.targetTexture = null;
RenderTexture.active = null;
Destroy(rt);
screenShotCamera.gameObject.SetActive(false);
 
cs
  1. 스크린샷 카메라를 준비합니다. MainCamera를 사용해도 무방하다면 MainCamera를 사용합니다.
  2. 화면 크기를 지정합니다.
  3. 저장할 이미지의 크기를 지정합니다. (특정 영역을 저장할 때 사용합니다. 그것이 아니라면 화면 크기와 동일하게 지정합니다.)
  4. 저장할 이미지의 Offset을 지정합니다.
  5. 참고 : http://chessire.tistory.com/entry/%EB%A0%8C%EB%8D%94%ED%85%8D%EC%8A%A4%EC%B3%90-%EC%A2%8C%ED%91%9C%EA%B3%84Render-Texture-coordinates
  6. RenderTexture에 screenShotCamera가 보고 있는 화면을 Render 합니다.
  7. RenderTexture를 Texture2D로 옮깁니다.
  8. 저장합니다.
  9. 뒷정리합니다.



Load

1
2
3
4
5
byte[] bytes = System.IO.File.ReadAllBytes(Application.persistentDataPath + "/filename.png");
Texture2D texture = new Texture2D(1, 1, TextureFormat.ARGB32, false);
texture.filterMode = FilterMode.Bilinear;
texture.LoadImage(bytes);
Sprite sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f));
cs



참고 : http://docs.unity3d.com/Manual/SL-PlatformDifferences.html



문제

오늘 스크린샷 기능 만들다가 Unity RenderTexture의 이상한 점을 발견했습니다.


바로 그래픽스 sdk에 따라 Coordinate system이 다르다는 사실...

참고 URL을 가보시면 이런 글을 확인하실 수 있습니다.



해결방법


1
2
3
4
5
if (SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.OpenGL2 ||
    SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.OpenGLCore ||
    SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.OpenGLES2 ||
    SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.OpenGLES3)
 
cs

위 조건일 때, y값을 반전시켜주시면 됩니다.

안드로이드폰에 apk파일을 adb로 인스톨할 때,


protocol fault couldn't read status에러가 발생하면 


cmd를 켜서 adb kill-server 명령어를 실행해주시면 됩니다.


adb는 [android sdk폴더/platform-tools]폴더 에 있습니다.

 (원래 기준점은 최하단 왼쪽 점을 고르게 되지만 PIXI는 DirectX처럼 y최소값이 상단에 위치합니다.)



Graham scan이란?
 Graham scan이란 유한개의 점 중 다른 점을 가둘 수 있는 외곽점을 찾는 알고리즘 중 하나입니다. 이 외곽점을 이으면 볼록 껍질(Convex hull)이 됩니다.

 시간 복잡도는 입니다.




알고리즘

  1. y 값이 가장 작은 점을 찾습니다.(만약 여러 개 존재시 x값이 가장 작은 점을 선택합니다.) 이 때, 이 점을 P0이라 부르겠습니다.
  2. P0을 기준으로 다른 모든 점의 각도를 구하여 각도가 작은 순서대로 정렬합니다.
  3. P0과 정렬된 점을 2개를 Convex hull에 추가합니다.
  4. 그 다음 점부터 다음 조건을 반복하여 수행합니다.
    1. Convex hull의 마지막 직선에서 현재 점이 왼쪽에 있으면 Convex hull의 마지막 점을 Convex hull에서 제외합니다.(현재 점이 2번 조건을 만족할 때까지 진행합니다.)
    2. Convex hull의 마지막 직선에서 현재 점이 오른쪽에 있으면 현재 점을 Convex hull에 추가하고 다음 점을 가져옵니다.
  5. Convex hull을 이루고 있는 점을 이어줍니다.

코드 설명
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
this.genConvexHull = function()
{
     // 점이 3개 미만이면 도형을 이룰 수 없음
    if(dots.length < 3)
        return;
    
    // (1) 기준점 선정
    var fiducialPoint = null;
    dots.forEach(function(item)
    {
        if(fiducialPoint === null)
        {
            fiducialPoint = item;
            return;
        }
 
        if(item.position.y < fiducialPoint.position.y ||
            (item.position.y == fiducialPoint.position.y && item.position.x < fiducialPoint.position.x))
            fiducialPoint = item;
    });
 
    // (2) 각도 순서대로 정렬
    var heap = new Heap(dots.length, function(a, b)
    {
        return a.angle < b.angle;
    });
 
    dots.forEach(function(item)
    {
        if(fiducialPoint == item)
            return;
 
        heap.push(
            {
                value:item,
                angle:Math.atan2(item.y - fiducialPoint.position.y, item.x - fiducialPoint.position.x),
            });
    });
 
    // (3) 기준점과 정렬된 앞의 두 점을 삽입하여 초기 Convex hull 구성
    var vertices = [];
    vertices.push(fiducialPoint);
    vertices.push(heap.pop().value);
    vertices.push(heap.pop().value);
 
    // (4) 순차적으로 조건을 실행
    var iter = heap.pop().value;
    while(true)
    {
        var prev1Point = vertices[vertices.length - 1];
        var prev2Point = vertices[vertices.length - 2];
        var prevVec = new PIXI.Vector(prev1Point.x - prev2Point.x, prev1Point.y - prev2Point.y);
 
        var currentVec = new PIXI.Vector(iter.position.x - prev2Point.position.x,
                                         iter.position.y - prev2Point.position.y);
 
        var dot = prevVec.x * currentVec.y - currentVec.x * prevVec.y;
        
        // (4-1) 점이 Convex hull 마지막 선분 기준으로 오른쪽에 있다면
        if(dot >= 0)
        {
            vertices.push(iter);
            if(heap.count() > 0)
                iter = heap.pop().value;
            else
                break;
        }
        // (4-2) 점이 Convex hull 마지막 선분 기준으로 왼쪽에 있다면
        else
            vertices.pop();
    }
 
    // (5) 점을 이어줌
    if(shape !== null)
        self.removeChild(shape);
 
    shape = new PIXI.Graphics();
    shape.lineStyle(2, 0xea5796, 1);
    shape.moveTo(vertices[0].position.x, vertices[0].position.y);
    for(var i = 1 ; i < vertices.length ; ++i)
        shape.lineTo(vertices[i].position.x, vertices[i].position.y);
    shape.lineTo(vertices[0].position.x, vertices[0].position.y);
 
    self.addChild(shape);
}
cs


(1) 기준점 선정(기준점 : P0)

  • y값이 가장 작은 점을 찾습니다.(OpenGL에서는 하단이지만 PIXI에서는 상단입니다. 어차피 모든 점이 같은 기준으로 선별되므로 추후 알고리즘에 영향을 미치지 않습니다.)
  • 최소 y값의 점이 여러개라면 최소 x값을 가진 점을 선정합니다.

(2) 각도 순서대로 정렬

  • Arctangent 함수를 통해 기준점과 나머지 점들의 각도를 구하여 정렬해줍니다.
  • 역으로 해도 똑같습니다. 그렇기 때문에 상단이든 하단이든 중요하지 않고 y 최소값으로 사용하도록 하겠습니다.
  • 정렬은 힙을 사용하였고, 이미지와 같이 dot1~dot6로 정렬하여 사용할 수 있게됩니다.

(3) 초기 Convex hull을 구성

  • P0, dot1, dot2로 비교를 위한 초기 Convex hull을 구성해줍니다. (dot2는 무조건 선분 p0, dot1의 오른쪽에 있게되므로 넣어줍니다.)


(4) 순차적으로 조건을 실행

  1. 점이 Convex hull 마지막 선분을 기준으로 오른쪽에 있다면 Convex hull에 추가하고 다음 점을 가져옴
  2. 점이 Convex hull 마지막 선분을 기준으로 왼쪽에 있다면 Convex hull 마지막 점을 제외

  • 이미지1, 이미지2 : dot3을 판별합니다.
    • 선분 dot1, dot2를 기준으로 dot3은 오른쪽에 있으니 dot2를 convex hull에서 제외하고 다음 루프로 넘어갑니다.
  • 이미지3, 이미지4 : 변경된 기준점으로 dot3을 재판별합니다.
    • dot4는 선분 p0, dot1 오른쪽에 있기 때문에 convex hull에 추가합니다.


(5) 점을 이어줌

  • 4번의 루프가 끝나면 convex hull이 완성되고 인접한 꼭지점끼리 이어줍니다.



두 선분의 좌우관계 구분

 두 선분의 좌우관계 구분은 벡터의 외적으로 판별할 수 있습니다.

(https://ko.wikipedia.org/wiki/%EB%B2%A1%ED%84%B0%EA%B3%B1)

a*b(a가 b의 왼쪽에 있음)를 할 경우 위쪽을 향하는 노멀 벡터를 가져올 수 있고,

b*a(b가 a의 왼쪽에 있음)를 할 경우에는 아래쪽을 향하는 노멀벡터를 가져올 수 있습니다.

이 성질을 이용하여 a, b의 z값이 0이라고 했을 때, a * b의 z값이 음수면 b는 a의 왼쪽에 있는 것이고, 양수면 b는 a의 오른쪽에 있다고 할 수 있습니다.

참고

 - https://ko.wikipedia.org/wiki/%ED%9E%99_(%EC%9E%90%EB%A3%8C_%EA%B5%AC%EC%A1%B0)


소스코드

heap.zip

사용하실 때, 댓글을 남겨주세요.


정의

 힙은 2진 힙(바이너리 힙)이라고도 부르며 최댓값과 최솟값 찾기 연산을 빠르게 하기 위해 고안된 완전이진트리를 기본으로 한 자료구조로써 다음과 같은 힙 속성(property)을 만족합니다.

 - A가 B의부모노드(parent node)이면 A의 키(key)값과 B의 키 값 사이에는 대소관계가 성립한다.


최대 힙

 - 부모노드의 키값이 자식 노드의 키값보다 항상 큰 힙


최소 힙

 - 부모노드의 키값이 자식 노드의 키값보다 항상 작은 힙


형제 사이에는 대소관계가 정해지지 않습니다.


(Max heap 이미지입니다.)



Push

 힙에 값을 추가하는 함수입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
this.push = function(value)
{
    arr[count] = value;
    var child = count;
 
    while(child > 0)
    {
        var parent = Math.floor((child + 1) * 0.5) - 1;
        if(compare(arr[child], arr[parent]))
        {
            swap(parent, child);
            child = parent;
        }
        else
            break;
    }
 
    ++count;
}
cs


규칙

  1. 마지막에 값을 추가합니다.
  2. 부모와 비교하여 부모보다 크면(최소힙은 부모보다 작으면) 부모와 swap합니다.
  3. 부모보다 작을때까지 혹은 최상단(0번 인덱스)이 될 때까지(최소힙은 부모보다 클때까지 2번을 반복합니다.


Pop

 힙에서 값을 가져오는 함수입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
this.pop = function()
{
    if(count === 0)
        return null;
 
    var ret = arr[0];
    arr[0] = arr[count-1];
 
    --count;
 
    var node = 0;
    while(true)
    {
        var child1 = node * 2 + 1;
        var child2 = child1 + 1;
 
        var swapIdx = node;
        if(child1 < count && compare(arr[child1], arr[swapIdx]))
            swapIdx = child1;
        if(child2 < count && compare(arr[child2], arr[swapIdx]))
            swapIdx = child2;
 
        if(swapIdx !== node)
        {
            swap(node, swapIdx);
            node = swapIdx;
        }
        else
            break;
    }
 
    return ret;
}
cs

규칙

  1. 최상단(0번인덱스)를 빼냅니다.
  2. 최상단에 배열의 마지막 데이터를 집어넣습니다.
  3. 자식과 비교하여 자신보다 크면 교체(최소힙일 경우 작으면)합니다.(둘 다 클 경우, 가장 큰 자식과 교체합니다.)
  4. 자식보다 작아질때(최소힙일 경우 커질때)까지 혹은 자식이 없어질 때까지 3번을 반복합니다.
이렇게 추출할 경우, 정렬된 데이터를 추출해낼 수 있습니다.


추가적으로

 보편적으로 compare함수를 생성자에서 전달받아 사용합니다.

 예를들어 int나 float이 아닌 사용자정의 형일 경우, 크거나 작은것을 비교할 수 없습니다. 그렇기 때문에 사용자정의 compare함수를 전달받아 사용하게 됩니다.



힙 정렬

 위에서 설명드린 힙을 이용해 정렬하는 알고리즘입니다.

제자리 정렬 알고리즘에 의 시간복잡도를 가지고 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{
    // (1) 힙 형태로 정렬
    for (var i = Math.floor(this.nodes.length * 0.5) - 1 ; i >= 0 ; --i)
    {
        var parent = i;
        while(true)
        {
            var child1 = parent * 2 + 1;
            var child2 = child1 + 1;
 
            var changeIndex = parent;
            if (child1 < this.nodes.length &&
                compare(this.nodes[child1], this.nodes[changeIndex]))
                changeIndex = child1;
            if (child2 < this.nodes.length &&
                compare(this.nodes[child2], this.nodes[changeIndex]))
                changeIndex = child2;
 
            if(changeIndex !== parent)
            {
                this.swap(parent, changeIndex);
                parent = changeIndex;
            }
            else
                break;
        }
    }
 
    // (2) 최상위 값을 빼와서 뒤에서부터 순서대로 삽입
    for (var i = 0 ; i < this.nodes.length ; ++i)
    {
        var last = this.nodes.length - 1 - i;
        var front = this.pop(last);
 
        this.nodes[last] = front;
        this.nodes[last].value.position.x = 5 + last * 55;
        this.nodes[last].value.position.y = 600;
    }
}
 
cs


(1) 힙 형태로 정렬

 일단 배열을 힙형태로 정렬을 해야합니다.

이때, 정렬은 자식을 가지고 있는 Math.floor(this.nodes.length * 0.5) - 1번째 인덱스부터 역순으로 정렬해 나갑니다.


(2) 최상위 값을 빼와서 뒤에서부터 순서대로 삽입

 제자리 정렬을 해야하기 때문에 힙용 배열을 따로 만드는 것이 아니라 맨 첫번째 있는 것을 pop하여 끝에 집어넣습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
this.prototype.pop = function(last)
{
    var ret = this.nodes[0];
 
    this.nodes[0] = this.nodes[last];
    this.nodes[0].value.position.x = 5;
    this.nodes[0].value.position.y = 600;
 
    if(last <= 1)
        return ret;
 
    var parent = 0;
    while(true)
    {
        var child1 = parent * 2 + 1;
        var child2 = child1 + 1;
 
        var changeIndex = parent;
        if (child1 < last &&
            compare(this.nodes[child1], this.nodes[changeIndex]))
            changeIndex = child1;
        if (child2 < last &&
            compare(this.nodes[child2], this.nodes[changeIndex]))
            changeIndex = child2;
 
        if(changeIndex !== parent)
        {
            this.swap(parent, changeIndex);
            parent = changeIndex;
        }
        else
            break;
    }
 
    return ret;
}
 
cs


pop함수는 위와 동일한 형태로 다른 부분은 last를 인자로 받아오는 것 외에는 없습니다.