본문 바로가기
  • You find inspiration to create your own path !
업무 자동화/FreeCAD

AI로 자동 설계 프로그램 만들기 - 간단한 단일 Shaft

by ToolBOX01 2026. 5. 29.
반응형

■ 설계자가 AI와 대화하며 자동 설계 프로그램 만들기

바이브 코딩(Vibe Coding)은 구체적인 문법이나 복잡한 코딩 기술을 몰라도, 개발자가 AI와 대화하며 "내가 원하는 느낌(Vibe)과 요구사항"을 설명하는 것만으로 소프트웨어를 개발하는 새로운 방식의 개발 트렌드입니다.

기존의 코딩이 개발자가 직접 코드를 한 줄씩 타이핑하는 방식이었다면, 바이브 코딩은 AI가 코딩을 전담하고 인간은 전체적인 방향성을 조율하는 '감독' 역할을 하는 것을 의미합니다.

자동 설계 프로그램 만들기

 

🛠️ 바이브 코딩의 주요 특징

  • 자연어 중심의 개발:
    Java, Python, C++ 같은 프로그래밍 언어 대신, "로그인 화면 만들어줘", "버튼 색상을 더 부드럽게 바꿔줘" 같은 평소 쓰는 말(자연어)로 프로그램을 만듭니다.
  • 초고속 프로토타이핑:
    아이디어를 설명하면 AI가 몇 초 만에 동작하는 웹페이지나 게임, 앱의 초기 모델(Prototype)을 뚝딱 만들어냅니다.
  • 개발 진입 장벽 완화:
    비전문가도 쉽게 소프트웨어를 만들 수 있게 되며, 기존 개발자들은 단순 반복 코딩에서 벗어나 아키텍처 설계나 비즈니스 로직 같은 더 중요한 문제에 집중할 수 있습니다.

 

💡 Gemini에서의 바이브 코딩 (Vibe Code)

Gemini 환경에서도 이러한 바이브 코딩을 강력하게 지원하는 전용 워크스페이스인 '캔버스(Canvas)' 기능을 제공하고 있습니다.

  •  Vibe Code 기능: 사용자가 아이디어나 요구사항을 입력하면 AI가 실시간으로 웹페이지, 소프트웨어 프로토타입, 간단한 게임(One Shot Arcade) 등을 화면 옆에 바로 구현해 줍니다.
  • 실시간 사이드바이사이드(Side-by-Side) 편집: 화면 한쪽에서는 AI와 대화하고, 다른 쪽에서는 코드가 실시간으로 수정되거나 실행 화면을 즉시 확인할 수 있습니다.
  • 외부 연동: 생성된 Python 코드는 클릭 한 번으로 Google Colab으로 내보내어 바로 실행 및 테스트해 볼 수 있습니다.
    FreeCAD는 외부에서 만든 코드를 복사하여 실행 할 수 있습니다. 별도의 아이콘을 만들어 사용 할수 있습니다.

 

 

바이브 코딩은 복잡한 문법 컴파일 에러와 싸우는 대신, AI와 티키타카 대화를 나누며 "내 느낌대로, 말하는 대로" 프로그램을 빌드해 나가는 미래형 개발 스타일입니다.  설계자 자신의 노하우가 담긴 자동설계 프로그램을 개발 할 수 있습니다.
노하우는 아마도 경험에거 나오는 데이터 일것 입니다. 어떤 조건일때 공차 값을 넣을것 입니다. 주석에 포함 되거나, 재질을 변경 할것 입니다. 이것은 IF문(조건문)과 연결되어 있고, 여러군데에 동시넣어야 하는 경우는 FOR문(반복문) 연결됩니다. 


■ 예시 Gemini  입력 프롬프트 (질문)

"FreeCAD 1.x 버전에 대응하는 Python 매크로 코드를 작성해줘. PartDesign 워크스페이스를 사용해서 원기둥(Cylinder)을 생성하고 수정할 수 있는 매크로여야 해.

[상세 요구사항]

  1. 모델링 방식: XY 평면에 사각형 스케치를 생성한 뒤, Y축을 기준으로 360도 회전(Revolution)시켜 원기둥을 만드는 방식을 사용해줘. 원점 고정 및 수직/수평 구속조건을 확실히 지정해줘.
  2. 치수 구속오류 방지: 나중에 코드로 치수를 변경할 때 인덱스가 꼬이지 않도록, 직경(반지름) 구속과 길이 구속을 추가하는 시점의 sketch.ConstraintCount를 활용해 각각의 정확한 구속조건 인덱스를 전역 변수나 상태 스크립트에 안전하게 기억하도록 짜줘.
  3. GUI 제공: 사용자가 실행할 수 있는 PySide6 기반의 매크로 다이얼로그(QDialog) 창을 띄워줘. 항상 위(WindowStaysOnTopHint) 옵션을 적용해줘.
  4. GUI 기능 구현:
    ① 모델 생성 그룹: 직경(Diameter)과 길이(Length)를 입력하는 스핀박스와 [생성] 버튼이 있어야 해. 생성 버튼을 누르면 새 문서에 모델이 만들어지고, 생성 버튼은 비활성화되며 수정 버튼이 활성화되어야 해.
    ② 모델 수정 그룹: 생성된 모델의 직경과 길이를 실시간으로 변경하고 적용할 수 있는 [수정] 버튼과 입력창을 따로 분리해줘. 수정 시 sketch.setDatum과 doc.recompute()를 사용해 화면에 바로 반영되어야 해.
  5. 사용자 경험: UI는 다크 모드 스타일(배경 #1A2535 등)로 세련되게 스타일시트(QSS)를 적용해 주고, 하단에 작업 상태를 알려주는 안내 레이블(Label)을 포함해줘. 모델 생성 및 수정이 끝나면 뷰를 Isometric(등각 투영)으로 전환하고 화면에 꽉 차게 피팅(ViewFit)하는 코드도 넣어줘.
  6. 재실행 방지: 매크로를 중복 실행했을 때 창이 여러 개 뜨지 않고, 이미 열려 있는 창이 있으면 앞으로 강조(raise)되도록 main() 진입점을 구성해줘."

코드

"""
FreeCAD Part Design - 원기둥 생성/수정 GUI 매크로
- FreeCAD 1.x (PySide6) 대응
- 치수 구속을 이름으로 저장하여 인덱스 오류 방지
"""

import FreeCAD
import FreeCADGui
import Part
import Sketcher

try:
    from PySide6 import QtWidgets, QtCore
except ImportError:
    from PySide2 import QtWidgets, QtCore

# ─────────────────────────────────────────────────────────────────────────────
# 전역 상태
# ─────────────────────────────────────────────────────────────────────────────
_state = {
    "doc":        None,
    "sketch":     None,
    "revolution": None,
    "created":    False,
    "idx_len":    -1,   # 길이 구속 인덱스
    "idx_rad":    -1,   # 반지름 구속 인덱스
}

# ─────────────────────────────────────────────────────────────────────────────
# XY_Plane 탐색
# ─────────────────────────────────────────────────────────────────────────────
def _find_xy_plane(doc):
    for obj in doc.Objects:
        if obj.Name == "XY_Plane" or obj.Label == "XY_Plane":
            return obj
    origin = doc.getObject("Origin")
    if origin and hasattr(origin, "OriginFeatures"):
        for child in origin.OriginFeatures:
            if "XY" in child.Label:
                return child
    return None

# ─────────────────────────────────────────────────────────────────────────────
# 생성
# ─────────────────────────────────────────────────────────────────────────────
def _build_cylinder(diameter: float, length: float):
    R = diameter / 2.0
    L = length

    doc  = FreeCAD.newDocument("Cylinder_Revolution")
    body = doc.addObject("PartDesign::Body", "Body")
    doc.recompute()

    sketch = body.newObject("Sketcher::SketchObject", "Sketch")
    doc.recompute()

    xy_plane = _find_xy_plane(doc)
    if xy_plane:
        try:
            sketch.AttachmentSupport = (xy_plane, [""])
        except Exception:
            try:
                sketch.Support = (xy_plane, [""])
            except Exception:
                pass
        sketch.MapMode = "FlatFace"
    doc.recompute()

    # ── 단면 사각형 (4개 선분) ────────────────────────────────────────
    # 0: P0(0,0)→P1(0,L)   회전축(Y축)
    # 1: P1(0,L)→P2(-R,L)  상단
    # 2: P2(-R,L)→P3(-R,0) 외경
    # 3: P3(-R,0)→P0(0,0)  하단
    sketch.addGeometry(Part.LineSegment(FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(0,  L, 0)), False)
    sketch.addGeometry(Part.LineSegment(FreeCAD.Vector(0, L, 0), FreeCAD.Vector(-R, L, 0)), False)
    sketch.addGeometry(Part.LineSegment(FreeCAD.Vector(-R, L, 0), FreeCAD.Vector(-R, 0, 0)), False)
    sketch.addGeometry(Part.LineSegment(FreeCAD.Vector(-R, 0, 0), FreeCAD.Vector(0,  0, 0)), False)

    # ── 구속 추가 (추가 순서대로 인덱스 0~) ──────────────────────────
    sketch.addConstraint(Sketcher.Constraint("Coincident", 0, 2, 1, 1))  # 0
    sketch.addConstraint(Sketcher.Constraint("Coincident", 1, 2, 2, 1))  # 1
    sketch.addConstraint(Sketcher.Constraint("Coincident", 2, 2, 3, 1))  # 2
    sketch.addConstraint(Sketcher.Constraint("Coincident", 3, 2, 0, 1))  # 3
    sketch.addConstraint(Sketcher.Constraint("Coincident", 0, 1, -1, 1)) # 4 원점고정
    sketch.addConstraint(Sketcher.Constraint("Vertical",   0))            # 5
    sketch.addConstraint(Sketcher.Constraint("Vertical",   2))            # 6
    sketch.addConstraint(Sketcher.Constraint("Horizontal", 1))            # 7
    sketch.addConstraint(Sketcher.Constraint("Horizontal", 3))            # 8

    # 치수 구속 — 추가 직전 현재 개수를 읽어 정확한 인덱스 저장
    idx_len = sketch.ConstraintCount   # = 9
    sketch.addConstraint(Sketcher.Constraint("DistanceY", 0, 1, 0, 2, L))

    idx_rad = sketch.ConstraintCount   # = 10
    sketch.addConstraint(Sketcher.Constraint("DistanceX", 3, 1, 3, 2, R))

    doc.recompute()

    # Revolution
    revolution = body.newObject("PartDesign::Revolution", "Revolution")
    revolution.Profile       = sketch
    revolution.ReferenceAxis = (sketch, ["V_Axis"])
    revolution.Angle         = 360.0
    try:
        revolution.Reversed = False
    except Exception:
        pass
    doc.recompute()

    try:
        FreeCADGui.activeDocument().activeView().viewIsometric()
        FreeCADGui.SendMsgToActiveView("ViewFit")
    except Exception:
        pass

    _state["doc"]        = doc
    _state["sketch"]     = sketch
    _state["revolution"] = revolution
    _state["created"]    = True
    _state["idx_len"]    = idx_len
    _state["idx_rad"]    = idx_rad

    FreeCAD.Console.PrintMessage(
        f"[Cylinder] 생성 완료 | 길이 구속 idx={idx_len}, 반지름 구속 idx={idx_rad}\n"
    )

# ─────────────────────────────────────────────────────────────────────────────
# 수정
# ─────────────────────────────────────────────────────────────────────────────
def _update_cylinder(new_diameter: float, new_length: float):
    sketch  = _state["sketch"]
    doc     = _state["doc"]
    idx_len = _state["idx_len"]
    idx_rad = _state["idx_rad"]
    R = new_diameter / 2.0
    L = new_length

    FreeCAD.Console.PrintMessage(
        f"[Cylinder] 수정 시도 | idx_len={idx_len}, idx_rad={idx_rad}, L={L}, R={R}\n"
    )

    sketch.setDatum(idx_len, FreeCAD.Units.Quantity(f"{L} mm"))
    sketch.setDatum(idx_rad, FreeCAD.Units.Quantity(f"{R} mm"))
    doc.recompute()

    try:
        FreeCADGui.activeDocument().activeView().viewIsometric()
        FreeCADGui.SendMsgToActiveView("ViewFit")
    except Exception:
        pass

# ─────────────────────────────────────────────────────────────────────────────
# GUI
# ─────────────────────────────────────────────────────────────────────────────
class CylinderDialog(QtWidgets.QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("원기둥 생성 / 수정")
        self.setFixedWidth(400)
        try:
            self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowType.WindowStaysOnTopHint)
        except AttributeError:
            self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
        self._build_ui()

    def _build_ui(self):
        root = QtWidgets.QVBoxLayout(self)
        root.setSpacing(12)
        root.setContentsMargins(16, 16, 16, 16)

        title = QtWidgets.QLabel("🔩  FreeCAD 원기둥 Revolution")
        self._center(title)
        title.setStyleSheet("font-size:14px; font-weight:bold; color:#4FC3F7;")
        root.addWidget(title)

        desc = QtWidgets.QLabel("XY 평면 · Y축 기준 회전 · 360°")
        self._center(desc)
        desc.setStyleSheet("font-size:11px; color:#78909C;")
        root.addWidget(desc)
        root.addWidget(self._hline())

        # ① 생성
        grp_c = QtWidgets.QGroupBox("① 모델 생성")
        grp_c.setStyleSheet(self._grp("#4FC3F7"))
        gl = QtWidgets.QFormLayout(grp_c)
        gl.setSpacing(8)
        self.inp_diam   = self._spin(1, 2000, 80,  "mm")
        self.inp_length = self._spin(1, 5000, 150, "mm")
        gl.addRow("직경 (Diameter) :", self.inp_diam)
        gl.addRow("길이 (Length)   :", self.inp_length)
        self.btn_create = QtWidgets.QPushButton("▶   생   성")
        self.btn_create.setFixedHeight(42)
        self.btn_create.setStyleSheet(self._btn("#1565C0", "#1E88E5"))
        self.btn_create.clicked.connect(self._on_create)
        gl.addRow(self.btn_create)
        root.addWidget(grp_c)

        # ② 수정
        grp_e = QtWidgets.QGroupBox("② 모델 수정")
        grp_e.setStyleSheet(self._grp("#81C784"))
        el = QtWidgets.QFormLayout(grp_e)
        el.setSpacing(8)
        self.inp_diam2   = self._spin(1, 2000, 80,  "mm")
        self.inp_length2 = self._spin(1, 5000, 150, "mm")
        el.addRow("새 직경 (Diameter) :", self.inp_diam2)
        el.addRow("새 길이 (Length)   :", self.inp_length2)
        self.btn_edit = QtWidgets.QPushButton("✏   수   정")
        self.btn_edit.setFixedHeight(42)
        self.btn_edit.setEnabled(False)
        self.btn_edit.setStyleSheet(self._btn("#2E7D32", "#43A047"))
        self.btn_edit.clicked.connect(self._on_edit)
        el.addRow(self.btn_edit)
        root.addWidget(grp_e)

        root.addWidget(self._hline())

        self.lbl_status = QtWidgets.QLabel("💡 직경과 길이를 입력 후 [생성] 버튼을 클릭하세요.")
        self.lbl_status.setWordWrap(True)
        self.lbl_status.setStyleSheet("color:#B0BEC5; font-size:11px;")
        root.addWidget(self.lbl_status)

        btn_close = QtWidgets.QPushButton("닫   기")
        btn_close.setFixedHeight(34)
        btn_close.setStyleSheet(self._btn("#455A64", "#607D8B"))
        btn_close.clicked.connect(self.close)
        root.addWidget(btn_close)

        self.setStyleSheet(
            "QDialog       { background:#1A2535; color:#ECEFF1; }"
            "QGroupBox      { color:#ECEFF1; font-weight:bold; font-size:12px; }"
            "QLabel         { color:#ECEFF1; }"
            "QDoubleSpinBox { background:#243447; color:#ECEFF1;"
            "                 border:1px solid #455A64; border-radius:4px; padding:4px; }"
            "QDoubleSpinBox::up-button, QDoubleSpinBox::down-button { background:#37474F; }"
        )

    def _on_create(self):
        d = self.inp_diam.value()
        l = self.inp_length.value()
        try:
            _build_cylinder(d, l)
            self.inp_diam2.setValue(d)
            self.inp_length2.setValue(l)
            self.btn_create.setEnabled(False)
            self.btn_edit.setEnabled(True)
            self._msg(f"✅ 생성 완료!  직경 {d} mm  /  길이 {l} mm\n"
                      f"   (길이구속 idx={_state['idx_len']}, 반지름구속 idx={_state['idx_rad']})\n"
                      "   ② 수정 탭에서 값을 변경 후 [수정]을 클릭하세요.", "#A5D6A7")
        except Exception as e:
            self._msg(f"❌ 생성 실패: {e}", "#EF9A9A")

    def _on_edit(self):
        d = self.inp_diam2.value()
        l = self.inp_length2.value()
        try:
            _update_cylinder(d, l)
            self._msg(f"✏ 수정 완료!  직경 {d} mm  /  길이 {l} mm", "#FFF59D")
        except Exception as e:
            self._msg(f"❌ 수정 실패: {e}", "#EF9A9A")

    def _msg(self, text, color="#B0BEC5"):
        self.lbl_status.setText(text)
        self.lbl_status.setStyleSheet(f"color:{color}; font-size:11px;")

    @staticmethod
    def _center(lbl):
        try:
            lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
        except AttributeError:
            lbl.setAlignment(QtCore.Qt.AlignCenter)

    @staticmethod
    def _spin(mn, mx, val, suf):
        sb = QtWidgets.QDoubleSpinBox()
        sb.setRange(mn, mx)
        sb.setValue(val)
        sb.setSuffix(f"  {suf}")
        sb.setDecimals(2)
        sb.setSingleStep(1.0)
        sb.setFixedHeight(30)
        return sb

    @staticmethod
    def _hline():
        f = QtWidgets.QFrame()
        try:
            f.setFrameShape(QtWidgets.QFrame.Shape.HLine)
        except AttributeError:
            f.setFrameShape(QtWidgets.QFrame.HLine)
        f.setStyleSheet("color:#2E3F50;")
        return f

    @staticmethod
    def _grp(c):
        return (f"QGroupBox {{ border:1px solid {c}55; border-radius:6px;"
                f"             margin-top:10px; padding:10px 8px 8px 8px; }}"
                f"QGroupBox::title {{ subcontrol-origin:margin; left:10px;"
                f"                    color:{c}; padding:0 4px; }}")

    @staticmethod
    def _btn(base, hover):
        return (f"QPushButton          {{ background:{base}; color:white; border:none;"
                f"                        border-radius:5px; font-size:13px; font-weight:bold; }}"
                f"QPushButton:hover    {{ background:{hover}; }}"
                f"QPushButton:pressed  {{ background:{base}; }}"
                f"QPushButton:disabled {{ background:#37474F; color:#546E7A; }}")


# ─────────────────────────────────────────────────────────────────────────────
# 진입점
# ─────────────────────────────────────────────────────────────────────────────
def main():
    for w in QtWidgets.QApplication.topLevelWidgets():
        if isinstance(w, CylinderDialog):
            w.raise_()
            w.activateWindow()
            return
    dlg = CylinderDialog(FreeCADGui.getMainWindow())
    dlg.show()

main()

 

프로그램 실행 화면

 

by korealionkk@gmail.com

반응형