본문 바로가기
  • You find inspiration to create your own path !
FreeCAD

FreeCAD & Python] 선택한 서피스 평면, 법선 벡터 값 가져오기

by ToolBOX01 2026. 9. 7.
반응형

■법선 벡터

 

법선

normal line normal은 '목수가 쓰는 곱자(직각자)'를 뜻하는 라틴어 어원 'norma'에서 유래하여,

namu.wiki

서피스(평면 등)의 법선 벡터 방향을 통해 3차원 공간에서 다음과 같은 다양한 핵심 정보들을 알 수 있습니다.

1. 면이 향하고 있는 방향 (면의 방위)

  • 평면이 3차원 공간 상에서 정확히 어느 쪽(위, 아래, 앞, 뒤, 혹은 특정 대각선 방향)을 바라보고 있는지 명확한 방위를 알 수 있습니다.

2. 기준 좌표계와의 수직 관계

  • 글로벌 좌표계를 기준으로 이 면이 어떤 축(X, Y, Z)에 평행하게 놓여 있는지 파악할 수 있습니다.
    (예를 들어, 법선 벡터가 (0, 0, 1)이라면 절대 좌표계의 Z축과 나란하며 정위치를 바라보고 있음을 뜻합니다.)

3. 솔리드의 외측과 내측(앞면과 뒷면) 구분

  • 3차원 형상에서 면의 바깥쪽 영역이 어디를 향하고 있는지 판별하여, 모델링 구조상 안과 밖을 구분하는 기준이 됩니다. 용용이 필요 합니다.

4. 돌출 및 가공 방향 결정 (CAD 실무)

FreeCAD의 Pad(보스 생성)나 포켓(파내기) 같은 기능을 실행할 때, 재료가 어느 방향으로 솟아오르거나 파여 들어갈지 결정하는 기준 축으로 직접 활용됩니다.


● 선택한 서피스 평면 법선 벡터 값 가져오는 코드

좌표의 기준은 "기본 좌표계"가 기준 입니다.

# -*- coding: utf-8 -*-

import FreeCAD as App
import FreeCADGui as Gui
import Part

from PySide import QtGui, QtCore


def log(text):
    print("[SurfaceNormal] " + str(text))


class NormalViewerDialog(QtGui.QDialog):

    def __init__(self):
        super(NormalViewerDialog, self).__init__()

        # 항상 앞에 표시 설정
        self.setWindowFlags(
            QtCore.Qt.Dialog |
            QtCore.Qt.WindowTitleHint |
            QtCore.Qt.WindowCloseButtonHint |
            QtCore.Qt.WindowStaysOnTopHint
        )

        self.setWindowTitle("서피스 법선 벡터 뷰어")
        self.resize(400, 320)

        self.selected_face_obj = None
        self.selected_face_name = None
        self.selected_face = None
        self.selection_active = False

        self.init_ui()

        # Selection 감시 타이머
        self.timer = QtCore.QTimer(self)
        self.timer.timeout.connect(self.check_selection)
        self.timer.start(100)

        # 항상 위 유지 타이머
        self.top_timer = QtCore.QTimer(self)
        self.top_timer.timeout.connect(self.keep_on_top)
        self.top_timer.start(500)

    def init_ui(self):
        layout = QtGui.QVBoxLayout()

        # Title
        title = QtGui.QLabel("3D 모델에서 서피스(Face)를 선택하세요.")
        title.setStyleSheet("font-weight:bold; font-size:13px;")
        layout.addWidget(title)

        # Selection Button & Label
        row = QtGui.QHBoxLayout()
        self.select_btn = QtGui.QPushButton("면(Face) 선택 시작")
        self.select_btn.clicked.connect(self.start_selection)
        
        self.target_label = QtGui.QLabel("미선택")
        self.target_label.setMinimumWidth(180)

        row.addWidget(self.select_btn)
        row.addWidget(self.target_label)
        layout.addLayout(row)

        # Results Group Box
        group_box = QtGui.QGroupBox("서피스 분석 정보")
        form_layout = QtGui.QFormLayout()

        self.type_label = QtGui.QLabel("-")
        self.normal_x_label = QtGui.QLabel("-")
        self.normal_y_label = QtGui.QLabel("-")
        self.normal_z_label = QtGui.QLabel("-")
        self.vector_str_label = QtGui.QLabel("-")

        form_layout.addRow("Surface 타입:", self.type_label)
        form_layout.addRow("법선 X:", self.normal_x_label)
        form_layout.addRow("법선 Y:", self.normal_y_label)
        form_layout.addRow("법선 Z:", self.normal_z_label)
        form_layout.addRow("벡터 전문:", self.vector_str_label)

        group_box.setLayout(form_layout)
        layout.addWidget(group_box)

        # Status Label
        self.status_label = QtGui.QLabel("버튼을 누르고 3D 화면의 면을 클릭하세요.")
        self.status_label.setStyleSheet("color:gray;")
        layout.addWidget(self.status_label)

        # Close Button
        self.close_btn = QtGui.QPushButton("닫기")
        self.close_btn.clicked.connect(self.close_dialog)
        layout.addWidget(self.close_btn)

        self.setLayout(layout)

    def keep_on_top(self):
        if not self.isVisible():
            return
        try:
            self.raise_()
        except:
            pass

    def start_selection(self):
        self.selection_active = True
        self.status_label.setText("▶ 3D 화면에서 분석할 Face를 클릭하십시오.")
        self.status_label.setStyleSheet("color:blue; font-weight:bold;")
        Gui.Selection.clearSelection()
        log("Face 선택 대기 중...")

    def check_selection(self):
        if not self.selection_active:
            return

        try:
            selection = Gui.Selection.getSelectionEx()
            if len(selection) != 1:
                return

            item = selection[0]
            if len(item.SubObjects) != 1:
                return

            obj = item.Object
            sub = item.SubObjects[0]
            name = item.SubElementNames[0]

            if not name.startswith("Face"):
                return

            # Surface 타입 확인
            surface = sub.Surface
            surface_type = type(surface).__name__

            # 법선 벡터 추출 (U, V 매개변수 기준 0,0 위치 또는 중앙값)
            normal = sub.normalAt(0, 0)

            # UI에 결과 반영
            self.selected_face_obj = obj
            self.selected_face_name = name
            self.selected_face = sub

            self.target_label.setText(f"{obj.Name}.{name}")
            self.type_label.setText(surface_type)
            self.normal_x_label.setText(f"{normal.x:.6f}")
            self.normal_y_label.setText(f"{normal.y:.6f}")
            self.normal_z_label.setText(f"{normal.z:.6f}")
            self.vector_str_label.setText(f"Vector({normal.x:.3f}, {normal.y:.3f}, {normal.z:.3f})")

            self.status_label.setText("✓ 법선 벡터 분석 완료!")
            self.status_label.setStyleSheet("color:green; font-weight:bold;")

            self.selection_active = False
            log(f"분석 완료 - 타입: {surface_type}, 법선: {normal}")

        except Exception as e:
            log("Selection Error: " + str(e))

    def close_dialog(self):
        try:
            self.timer.stop()
            self.top_timer.stop()
        except:
            pass
        self.close()

    def closeEvent(self, event):
        try:
            self.timer.stop()
            self.top_timer.stop()
        except:
            pass
        event.accept()


def main():
    if App.ActiveDocument is None:
        QtGui.QMessageBox.warning(
            Gui.getMainWindow(),
            "오류",
            "먼저 FreeCAD Document를 열어주십시오."
        )
        return

    global normal_dialog
    try:
        normal_dialog.close()
    except:
        pass

    normal_dialog = NormalViewerDialog()
    normal_dialog.show()
    normal_dialog.raise_()
    normal_dialog.activateWindow()

main()

 


by korealionkk@gmail.com

반응형