Skip to content

Conversation

@yiranwu0
Copy link
Contributor

@yiranwu0 yiranwu0 commented Nov 5, 2023

Why are these changes needed?

  • Add Contrib test for math proxy agent.
  • remove [mathchat] dependency in build test flow.
  • Putting tests for different contrib agents in one job. Previous test fail will not block sequential tests.

Example that previous test fails doesn’t block sequential tests
https://github.com/microsoft/autogen/actions/runs/6759692725/job/18372611806

(Install packages and dependencies for RetrieveChat -> fails
Run “Install packages and dependencies for MathChat” -> success
Run “test MathChat” -> success
Result is still error
)

When previous dependency fails, corresponding testing will be disabled.
https://github.com/microsoft/autogen/actions/runs/6759701280/job/18372633270?pr=555
(Run “ Install packages and dependencies for RetrieveChat” -> fail
Run “”Install packages and dependencies for MathChat ->fail
skipping test MathChat)

Related issue number

Checks

@codecov-commenter
Copy link

codecov-commenter commented Nov 5, 2023

Codecov Report

Merging #555 (9735537) into main (fda7a39) will increase coverage by 4.88%.
The diff coverage is n/a.

@@            Coverage Diff             @@
##             main     #555      +/-   ##
==========================================
+ Coverage   32.40%   37.29%   +4.88%     
==========================================
  Files          27       27              
  Lines        3357     3357              
  Branches      756      756              
==========================================
+ Hits         1088     1252     +164     
+ Misses       2173     1989     -184     
- Partials       96      116      +20     
Flag Coverage Δ
unittests 37.23% <ø> (+4.88%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

see 3 files with indirect coverage changes

AZURE_OPENAI_API_BASE: ${{ secrets.AZURE_OPENAI_API_BASE }}
OAI_CONFIG_LIST: ${{ secrets.OAI_CONFIG_LIST }}
run: |
coverage run -a -m pytest test/agentchat/contrib/test_math_user_proxy_agent.py
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the output of coverage run will overwrite the output of the former step. In the end, only one step of coverage will be uploaded.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add different names to the output of different steps and combine them at the upload step?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The coverage run -a will append outputs to the same file(.coverage), so we are good with this. Not very sure but this is what I find.


jobs:
RetrieveChatTest:
OpenAI4ContribTests:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One problem with this approach is that the previous installed dependencies will remain in the later steps. That makes the test environment not clean for later steps. Is it possible to reset the environment for later steps?

Copy link
Contributor Author

@yiranwu0 yiranwu0 Nov 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't found any good way to clear up the environment. A possible solution is to use virtual env, but different os needs different syntax, which makes it complex. The other way is just putting them in different jobs.

I guess there are two problems to consider here:

  1. Is it ok to NOT having a clean env? Will there be a case that user does pip install autogen[retrievechat, mathchat], and they expect things just work?
  2. I suggested that separating the jobs would be tedious to look in github actions, which is why I change this in the first place. But we might need to revisit the problem: do you think it is indeed a issue, or we can bear with it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's pretty important for the automatic jobs to test different contributed agents in different environments, each with just its own dependencies, whether this is done through separate virtual envs, different jobs, or some other mechanism. Wish I had more experience in setting up such tests.

Copy link
Contributor Author

@yiranwu0 yiranwu0 Nov 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

coverage run -a -m pytest test/test_retrieve_utils.py test/agentchat/contrib

will test all files in the contrib folder, while only dependencies for Retrievechat is installed. This will result in error in the future if a contrib agent requires other packages to be installed.
https://github.com/microsoft/autogen/blob/fda7a39dd9ede1c115d37c2a454f55b7f22c8193/.github/workflows/contrib-tests.yml#L52C38-L52C38

@yiranwu0 yiranwu0 mentioned this pull request Nov 25, 2023
3 tasks
@yiranwu0 yiranwu0 closed this Mar 7, 2024
SHAM560 added a commit to SHAM560/autogen that referenced this pull request Dec 20, 2025
Здесь сделай чтоб присадку делал согласно локальных координат компонентов, так как сейчас на повёрнутых под определённый градус угла компонентах не делает присадок потому что делает присадку согласно мировых координат что не допустимо. Пиши полный код целиком.


require 'sketchup.rb'
require 'extensions.rb'

module ABFStyleDrill
  # Настройки по умолчанию
  DEFAULTS = {
    :cam_diameter => 8.0,   # Диаметр отверстия в пласть (Cam)
    :screw_diameter => 5.0, # Диаметр отверстия в торец (Screw)
    :offset => 50.0,        # Отступ от края детали для присадки
    :hole_count => 2,       # Количество присадочных отверстий
    :marker_length => 15.0, # Длина проекции (маркера) для торцевого отверстия

    # --- НОВЫЕ НАСТРОЙКИ ДЛЯ ПЕТЕЛЬ ---
    :hinge_diameter => 35.0, # Диаметр отверстия под чашку петли (35мм)
    :hinge_offset => 100.0,  # Отступ от края детали для первой петли (100мм) - Смещение по длине
    :hinge_count => 2,       # Количество петель
    :hinge_side_offset => 21.5, # Смещение от бокового края компонента (21.5мм)
    :hinge_screw_diameter => 5.0, # Диаметр отверстий под саморезы (5мм)
    :hinge_screw_spacing => 45.0, # Расстояние между центрами отверстий под саморезы (45мм)
    :hinge_screw_vertical_offset => 9.5, # НОВОЕ: Смещение отверстий под саморезы от центра чашки (9.5мм)

    # --- НОВЫЕ НАСТРОЙКИ ДЛЯ ПЛАНКИ ---
    :strip_diameter => 5.0,        # Диаметр отверстия под планку (5мм)
    :strip_spacing => 32.0,        # Межосевое расстояние (32мм)
    :strip_side_offset => 37.0,     # Смещение от края боковины (37мм)
    :strip_offset => 100.0         # НОВОЕ: Отступ от короткого края (100мм)
  }

  # Константа для зазора между деталями при раскладке
  GAP = 500.0.mm

  # Переменная для отслеживания состояния аннотаций
  @annotations_visible = false

  # --- МЕТОД: Очистка ранее созданных аннотаций (размеров) ---
  def self.cleanup_annotations(entities)
    entities_to_remove = []
    entities.each do |e|
      if e.is_a?(Sketchup::DimensionLinear) && e.get_attribute('ABFDrill', 'Type') == 'Dimension'
        entities_to_remove << e
      end
    end
    entities.erase_entities(entities_to_remove) if entities_to_remove.any?
  end

  # --- МЕТОД: Очистка только стандартных присадочных отверстий и маркеров ---
  def self.cleanup_standard_drill_geometry(entity)
    defn = entity.respond_to?(:definition) ? entity.definition : entity.entities.parent
    entities_to_remove = []

    defn.entities.each do |e|
      # Удаляем всё, что помечено как DrillHole
      type = e.get_attribute('ABFDrill', 'Type')
      if type == 'DrillHole'
        entities_to_remove << e
      end
    end

    defn.entities.erase_entities(entities_to_remove) if entities_to_remove.any?
  end

  # --- МЕТОД: Очистка только отверстий под петли ---
  def self.cleanup_hinge_drill_geometry(entity)
    defn = entity.respond_to?(:definition) ? entity.definition : entity.entities.parent
    entities_to_remove = []

    defn.entities.each do |e|
      # Удаляем всё, что помечено как HingeHole
      type = e.get_attribute('ABFDrill', 'Type')
      if type == 'HingeHole'
        entities_to_remove << e
      end
    end

    defn.entities.erase_entities(entities_to_remove) if entities_to_remove.any?
  end

  # --- НОВЫЙ МЕТОД: Очистка только отверстий под планку ---
  def self.cleanup_strip_drill_geometry(entity)
    defn = entity.respond_to?(:definition) ? entity.definition : entity.entities.parent
    entities_to_remove = []

    defn.entities.each do |e|
      # Удаляем всё, что помечено как StripHole (включая новую точку)
      type = e.get_attribute('ABFDrill', 'Type')
      if type == 'StripHole'
        entities_to_remove << e
      end
    end

    defn.entities.erase_entities(entities_to_remove) if entities_to_remove.any?
  end

  # --- МЕТОД: Отображение диалогового окна (Обновлено для передачи параметров петель в планку) ---
  def self.show_dialog
    dialog = UI::HtmlDialog.new(
      {
        :dialog_title => "ABF Style Drill & Hinges",
        :preferences_key => "com.abfstyle.drill",
        :scrollable => true,
        :resizable => true,
        :width => 350,
        :height => 850, # Увеличена высота для новых настроек
        :style => UI::HtmlDialog::STYLE_DIALOG
      }
    )

    html_content = <<-HTML
      <!DOCTYPE html>
      <html>
      <head>
        <style>
          body { font-family: 'Segoe UI', sans-serif; padding: 20px; background-color: microsoft#333; color: white; }
          h3 { margin-top: 20px; color: #ff9800; border-bottom: 1px solid microsoft#555; padding-bottom: 10px; }
          .control { margin-bottom: 15px; }
          label { display: flex; justify-content: space-between; align-items: center; font-size: 14px; }
          input { width: 80px; padding: 5px; border-radius: 4px; border: none; text-align: center; }
          button {
            width: 100%; padding: 12px; margin-top: 10px;
            background-color: #ff9800; color: white; border: none;
            border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 16px;
            transition: background 0.3s;
          }
          button:hover { background-color: #e68900; }
          .hint { font-size: 11px; color: #aaa; margin-top: 5px; }
          .secondary-button { background-color: microsoft#555; }
          .secondary-button:hover { background-color: microsoft#777; }
          .annotation-button { background-color: #00bcd4; }
          .annotation-button:hover { background-color: #0097a7; }
          .hinge-button { background-color: #4CAF50; }
          .hinge-button:hover { background-color: #45a049; }
          /* НОВЫЙ СТИЛЬ ДЛЯ КНОПКИ ПЛАНКИ */
          .strip-button { background-color: #9c27b0; }
          .strip-button:hover { background-color: #7b1fa2; }
        </style>
      </head>
      <body>
        <!-- --- НОВАЯ СЕКЦИЯ ДЛЯ ПЛАНКИ (ПЕРЕМЕЩЕНА ВЫШЕ) --- -->
        <h3>Присадка планки</h3>
        <div class="control">
          <label>Диаметр отверстий (мм) <input type="number" id="strip_dia" value="#{DEFAULTS[:strip_diameter]}" step="0.5"></label>
        </div>
        <div class="control">
          <label>Межосевое (мм) <input type="number" id="strip_spacing" value="#{DEFAULTS[:strip_spacing]}" step="1.0"></label>
        </div>
        <div class="control">
          <label>Смещение от края (мм) <input type="number" id="strip_side_off" value="#{DEFAULTS[:strip_side_offset]}" step="1.0"></label>
        </div>
        <!-- НОВЫЙ ИНПУТ ДЛЯ СМЕЩЕНИЯ ПЛАНКИ -->
        <div class="control">
          <label>Смещение по длине (мм) <input type="number" id="strip_off" value="#{DEFAULTS[:strip_offset]}" step="1.0"></label>
          <div class="hint">Отступ первой планки от короткого края.</div>
        </div>
        <button class="strip-button" onclick="run_strip_drill()">ПРИСАДКА ПЛАНКИ</button>
        <!-- --- КОНЕЦ НОВОЙ СЕКЦИИ --- -->

        <h3>Настройки присадки (Minifix)</h3>

        <div class="control">
          <label>Количество отверстий <input type="number" id="hole_count" value="#{DEFAULTS[:hole_count]}" step="1" min="1" oninput="update_model_realtime()"></label>
          <div class="hint">Общее количество на стык.</div>
        </div>

        <div class="control">
          <label>Диаметр (Пласть/Cam) <input type="number" id="cam_dia" value="#{DEFAULTS[:cam_diameter]}" step="0.5" oninput="update_model_realtime()"></label>
        </div>

        <div class="control">
          <label>Диаметр (Торец/Screw) <input type="number" id="screw_dia" value="#{DEFAULTS[:screw_diameter]}" step="0.5" oninput="update_model_realtime()"></label>
        </div>

        <div class="control">
          <label>Длина маркера (мм) <input type="number" id="marker_len" value="#{DEFAULTS[:marker_length]}" step="1.0" min="0" oninput="update_model_realtime()"></label>
          <div class="hint">Длина проекции торцевого отверстия.</div>
        </div>

        <div class="control">
          <label>Отступ от края (мм) <input type="number" id="off" value="#{DEFAULTS[:offset]}" step="1" oninput="update_model_realtime()"></label>
        </div>

        <button onclick="run()">ПРИСАДИТЬ (Auto)</button>

        <!-- --- НОВАЯ СЕКЦИЯ ДЛЯ ПЕТЕЛЬ --- -->
        <h3>Настройки петель</h3>

        <div class="control">
          <label>Количество кругов <input type="number" id="hinge_count" value="#{DEFAULTS[:hinge_count]}" step="1" min="1"></label>
        </div>

        <div class="control">
          <label>Диаметр чашки (мм) <input type="number" id="hinge_dia" value="#{DEFAULTS[:hinge_diameter]}" step="1.0"></label>
        </div>

        <!-- НОВЫЕ ИНПУТЫ ДЛЯ ОТВЕРСТИЙ ПОД САМОРЕЗЫ -->
        <div class="control">
          <label>Диаметр саморезов (мм) <input type="number" id="hinge_screw_dia" value="#{DEFAULTS[:hinge_screw_diameter]}" step="0.5"></label>
        </div>

        <div class="control">
          <label>Расстояние между (мм) <input type="number" id="hinge_screw_spacing" value="#{DEFAULTS[:hinge_screw_spacing]}" step="1.0"></label>
          <div class="hint">Расстояние между центрами отверстий под саморезы (45мм).</div>
        </div>

        <!-- НОВЫЙ ИНПУТ ДЛЯ СМЕЩЕНИЯ САМОРЕЗОВ ОТНОСИТЕЛЬНО ЧАШКИ -->
        <div class="control">
          <label>Смещение саморезов (мм) <input type="number" id="hinge_screw_vert_off" value="#{DEFAULTS[:hinge_screw_vertical_offset]}" step="0.5"></label>
          <div class="hint">Смещение отверстий от центра чашки (вдоль 21.5мм оси).</div>
        </div>
        <!-- КОНЕЦ НОВЫХ ИНПУТОВ -->

        <div class="control">
          <label>Смещение по длине (мм) <input type="number" id="hinge_off" value="#{DEFAULTS[:hinge_offset]}" step="1.0"></label>
          <div class="hint">Отступ первой петли от короткого края.</div>
        </div>

        <div class="control">
          <label>Смещение от края компонента (мм) <input type="number" id="hinge_side_off" value="#{DEFAULTS[:hinge_side_offset]}" step="0.5"></label>
          <div class="hint">Смещение центра чашки от бокового края (21.5мм).</div>
        </div>

        <button class="hinge-button" onclick="run_hinges()">ПЕТЛИ</button>
        <!-- --- КОНЕЦ НОВОЙ СЕКЦИИ --- -->

        <button class="secondary-button" onclick="line_up()">РАЗЛОЖИТЬ</button>
        <button class="annotation-button" onclick="show_annotations()">АННОТАЦИИ</button>

        <script>
          function debounce(func, timeout = 300) {
            let timer;
            return (...args) => {
              clearTimeout(timer);
              timer = setTimeout(() => { func.apply(this, args); }, timeout);
            };
          }

          function get_input_values() {
            return {
              cam_dia: parseFloat(document.getElementById('cam_dia').value),
              screw_dia: parseFloat(document.getElementById('screw_dia').value),
              off: parseFloat(document.getElementById('off').value),
              hole_count: parseInt(document.getElementById('hole_count').value),
              marker_len: parseFloat(document.getElementById('marker_len').value)
            };
          }

          // --- НОВАЯ ФУНКЦИЯ ДЛЯ ПОЛУЧЕНИЯ ЗНАЧЕНИЙ ПЕТЕЛЬ (ОБНОВЛЕНО) ---
          function get_hinge_values() {
            return {
              hinge_count: parseInt(document.getElementById('hinge_count').value),
              hinge_dia: parseFloat(document.getElementById('hinge_dia').value),
              hinge_off: parseFloat(document.getElementById('hinge_off').value),
              hinge_side_off: parseFloat(document.getElementById('hinge_side_off').value),
              // НОВЫЕ ЗНАЧЕНИЯ
              hinge_screw_dia: parseFloat(document.getElementById('hinge_screw_dia').value),
              hinge_screw_spacing: parseFloat(document.getElementById('hinge_screw_spacing').value),
              hinge_screw_vertical_offset: parseFloat(document.getElementById('hinge_screw_vert_off').value)
            };
          }

          // --- НОВАЯ ФУНКЦИЯ ДЛЯ ПОЛУЧЕНИЯ ЗНАЧЕНИЙ ПЛАНКИ (ОБНОВЛЕНО) ---
          function get_strip_values() {
            return {
              strip_dia: parseFloat(document.getElementById('strip_dia').value),
              strip_spacing: parseFloat(document.getElementById('strip_spacing').value),
              strip_side_off: parseFloat(document.getElementById('strip_side_off').value),
              strip_offset: parseFloat(document.getElementById('strip_off').value), // ДОБАВЛЕНО
              // ДОБАВЛЕНЫ ЗНАЧЕНИЯ ПЕТЕЛЬ ДЛЯ ПОЗИЦИОНИРОВАНИЯ ПЛАНКИ
              hinge_count: parseInt(document.getElementById('hinge_count').value)
            };
          }

          function run() {
            var values = get_input_values();
            sketchup.do_drill(values.cam_dia, values.screw_dia, values.off, values.hole_count, values.marker_len);
          }

          // --- НОВАЯ ФУНКЦИЯ ДЛЯ ЗАПУСКА ПЕТЕЛЬ (ОБНОВЛЕНО) ---
          function run_hinges() {
            var values = get_hinge_values();
            // Передаем новые значения
            sketchup.do_hinges(
              values.hinge_count,
              values.hinge_dia,
              values.hinge_off,
              values.hinge_side_off,
              values.hinge_screw_dia,
              values.hinge_screw_spacing,
              values.hinge_screw_vertical_offset
            );
          }

          // --- НОВАЯ ФУНКЦИЯ ДЛЯ ЗАПУСКА ПЛАНКИ (ОБНОВЛЕНО) ---
          function run_strip_drill() {
            var values = get_strip_values();
            sketchup.do_strip_drill(
              values.strip_dia,
              values.strip_spacing,
              values.strip_side_off,
              // ПЕРЕДАЕМ ПАРАМЕТРЫ ДЛЯ ПОЗИЦИОНИРОВАНИЯ
              values.hinge_count,
              values.strip_offset // ИСПОЛЬЗУЕМ НОВОЕ СМЕЩЕНИЕ
            );
          }

          const debounced_update_callback = debounce(function() {
            var values = get_input_values();
            sketchup.update_drill_realtime(values.cam_dia, values.screw_dia, values.off, values.hole_count, values.marker_len);
          }, 300);

          function update_model_realtime() { debounced_update_callback(); }
          function line_up() { sketchup.line_up_components(); }
          function show_annotations() { sketchup.show_drill_annotations(); }
          window.onload = function() { update_model_realtime(); };
        </script>
      </body>
      </html>
    HTML

    dialog.set_html(html_content)
    dialog.center

    dialog.add_action_callback("do_drill") do |ctx, cam_dia, screw_dia, off, hole_count, marker_len|
      self.process_model(cam_dia, screw_dia, off, hole_count, marker_len)
    end

    dialog.add_action_callback("update_drill_realtime") do |ctx, cam_dia, screw_dia, off, hole_count, marker_len|
      self.process_model(cam_dia, screw_dia, off, hole_count, marker_len)
    end

    # --- CALLBACK ДЛЯ ПЕТЕЛЬ ---
    dialog.add_action_callback("do_hinges") do |ctx, count, diameter, length_offset, side_offset, screw_diameter, screw_spacing, screw_vert_offset|
      self.process_hinge_holes(count, diameter, length_offset, side_offset, screw_diameter, screw_spacing, screw_vert_offset)
    end

    # --- НОВЫЙ CALLBACK ДЛЯ ПЛАНКИ (ОБНОВЛЕНО) ---
    # Изменено: hinge_offset заменен на length_offset (который теперь strip_offset)
    dialog.add_action_callback("do_strip_drill") do |ctx, diameter, spacing, side_offset, count, length_offset|
      self.process_strip_holes(diameter, spacing, side_offset, count, length_offset)
    end

    dialog.add_action_callback("line_up_components") { |ctx| self.line_up_components }
    dialog.add_action_callback("show_drill_annotations") { |ctx| self.show_drill_annotations }

    dialog.show
  end

  # --- ГЛАВНЫЙ ПРОЦЕСС ПРИСАДКИ (Minifix) ---
  def self.process_model(cam_diameter_mm, screw_diameter_mm, offset_mm, hole_count, marker_length_mm)
    model = Sketchup.active_model
    selection = model.selection
    items = selection.grep(Sketchup::ComponentInstance) + selection.grep(Sketchup::Group)

    # Если выделено меньше 2-х, очищаем присадки только на выделенных и выходим
    if items.length < 2
      model.start_operation("ABF Drill Cleanup", true)
      items.each { |ent| self.cleanup_standard_drill_geometry(ent) }
      model.commit_operation
      return
    end

    model.start_operation("ABF Style Drill Update", true)
    # Очищаем только стандартные присадки на выделенных компонентах
    items.each { |ent| self.cleanup_standard_drill_geometry(ent) }

    processed_pairs = 0
    (0...items.length).each do |i|
      ((i + 1)...items.length).each do |j|
        ent1 = items[i]
        ent2 = items[j]
        # Проверяем пересечение
        if ent1.bounds.intersect(ent2.bounds).valid?
           if self.apply_drill_to_pair(ent1, ent2, cam_diameter_mm.mm, screw_diameter_mm.mm, offset_mm.mm, hole_count, marker_length_mm.mm)
             processed_pairs += 1
           end
        end
      end
    end
    model.commit_operation
  end

  # ... (get_diameter остается без изменений)
  def self.get_diameter(entity, global_drill_axis, cam_dia, screw_dia)
    trans_inv = entity.transformation.inverse
    local_drill_axis = global_drill_axis.transform(trans_inv)
    bb_local = entity.bounds
    w, h, d = bb_local.width, bb_local.height, bb_local.depth
    dims = { w => Geom::Vector3d.new(1, 0, 0), h => Geom::Vector3d.new(0, 1, 0), d => Geom::Vector3d.new(0, 0, 1) }
    min_dim = dims.keys.min
    local_thickness_axis = dims[min_dim]
    dot_product = local_drill_axis.dot(local_thickness_axis).abs
    return (dot_product > 0.999) ? cam_dia : screw_dia
  end

  # --- ОСНОВНАЯ ЛОГИКА ПРИСАДКИ И ГЕОМЕТРИИ (Minifix) ---
  def self.apply_drill_to_pair(ent1, ent2, cam_diameter, screw_diameter, offset, hole_count, marker_length)
    bb1 = ent1.bounds
    bb2 = ent2.bounds
    intersect = bb1.intersect(bb2)
    return false if intersect.empty?

    w, h, d = intersect.width, intersect.height, intersect.depth
    dims = [w, h, d]
    min_dim = dims.min
    return false if dims.max < 10.mm

    drill_axis = Geom::Vector3d.new(0,0,1)
    long_axis_vec = Geom::Vector3d.new(1,0,0)
    center = intersect.center

    if min_dim == w
      drill_axis = Geom::Vector3d.new(1, 0, 0)
      long_axis_vec = (h > d) ? Geom::Vector3d.new(0, 1, 0) : Geom::Vector3d.new(0, 0, 1)
      len = [h, d].max
    elsif min_dim == h
      drill_axis = Geom::Vector3d.new(0, 1, 0)
      long_axis_vec = (w > d) ? Geom::Vector3d.new(1, 0, 0) : Geom::Vector3d.new(0, 0, 1)
      len = [w, d].max
    else
      drill_axis = Geom::Vector3d.new(0, 0, 1)
      long_axis_vec = (w > h) ? Geom::Vector3d.new(1, 0, 0) : Geom::Vector3d.new(0, 1, 0)
      len = [w, h].max
    end

    drill_points = []
    hole_count = [1, hole_count.to_i].max
    max_drill_length = len - (offset * 2)

    if hole_count == 1 || max_drill_length < 0.mm
      drill_points << center
    else
      spacing = max_drill_length / (hole_count - 1).to_f
      start_dist_from_center = (len / 2.0) - offset
      start_point = center - long_axis_vec.clone.tap { |v| v.length = start_dist_from_center }
      (0...hole_count).each do |i|
        current_offset = long_axis_vec.clone.tap { |v| v.length = i * spacing }
        drill_points << start_point + current_offset
      end
    end

    [ent1, ent2].each do |ent|
      defn = ent.respond_to?(:definition) ? ent.definition : ent.entities.parent
      trans_inv = ent.transformation.inverse
      current_diameter = self.get_diameter(ent, drill_axis, cam_diameter, screw_diameter)

      drill_points.each do |global_pt|
        local_pt = global_pt.transform(trans_inv)
        local_axis = drill_axis.transform(trans_inv)

        # Вынос на внешнюю сторону для отверстий в пласть
        if current_diameter == cam_diameter
          bounds = defn.bounds
          bounds_center = bounds.center
          axis_idx = local_axis.to_a.map(&:abs).each_with_index.max[1]
          dir_val = bounds_center[axis_idx] - local_pt[axis_idx]
          dir_val = 1.0 if dir_val.abs < 0.0001
          target_coordinate = (dir_val > 0) ? bounds.max[axis_idx] : bounds.min[axis_idx]
          local_pt[axis_idx] = target_coordinate
        end

        # 1. Создаем само отверстие (круг)
        circle = defn.entities.add_circle(local_pt, local_axis, current_diameter / 2.0)
        cpoint = defn.entities.add_cpoint(local_pt)
        circle.each { |e| e.set_attribute('ABFDrill', 'Type', 'DrillHole') }
        cpoint.set_attribute('ABFDrill', 'Type', 'DrillHole')

        # 2. Логика для рисования прямоугольника-проекции (только линии)
        if current_diameter == screw_diameter && marker_length > 0.0.mm
           bounds = defn.bounds

           # Определяем ось толщины (самая короткая сторона)
           b_dims = [bounds.width, bounds.height, bounds.depth]
           min_b_dim = b_dims.min
           thick_axis_idx = b_dims.index(min_b_dim)

           # Вектор нормали к грани (ось толщины)
           face_normal = Geom::Vector3d.new(0,0,0)
           face_normal[thick_axis_idx] = 1

           # Вектор "ширины" прямоугольника
           width_vec = local_axis.cross(face_normal)

           if width_vec.length > 0.001
             width_vec.length = current_diameter / 2.0

             # Вектор "длины" маркера, используем значение из инпута
             marker_len_vec = local_axis.clone

             # Направление: Внутрь детали
             vec_to_center = bounds.center - local_pt
             if vec_to_center.dot(marker_len_vec) < 0
                marker_len_vec.reverse!
             end
             marker_len_vec.length = marker_length

             # Рисуем прямоугольники на обеих плоскостях
             faces_coords = [bounds.min[thick_axis_idx], bounds.max[thick_axis_idx]]

             faces_coords.each do |z_coord|
                pt_on_face = local_pt.clone
                pt_on_face[thick_axis_idx] = z_coord

                # 4 точки прямоугольника
                p1 = pt_on_face + width_vec
                p2 = pt_on_face - width_vec
                p3 = p2 + marker_len_vec
                p4 = p1 + marker_len_vec

                # Создаем группу для маркера
                m_group = defn.entities.add_group
                # Создаем Face, чтобы потом его удалить, но сохранить Edges
                m_face = m_group.entities.add_face(p1, p2, p3, p4)

                if m_face
                   # УДАЛЯЕМ ГРАНЬ, ОСТАВЛЯЯ ТОЛЬКО ЛИНИИ (Edges)
                   m_face.erase!
                   # Метим группу как DrillHole для очистки
                   m_group.set_attribute('ABFDrill', 'Type', 'DrillHole')
                else
                   # Если грань не создалась, удаляем пустую группу
                   m_group.erase!
                end
             end
           end
        end
        # --- КОНЕЦ ЛОГИКИ ПРЯМОУГОЛЬНИКА ---
      end
    end
    return true
  end

  # --- ИСПРАВЛЕННЫЙ МЕТОД: Рисование компенсированного круга ---
  # Этот метод создает круг, который останется кругом даже после не-униформного масштабирования
  def self.draw_drill_hole_geometry(entity, center_pt, radius, normal_axis, type_tag)
    defn_entities = entity.respond_to?(:definition) ? entity.definition.entities : entity.entities
    trans = entity.transformation
    
    # 1. Получаем обратную трансформацию для компенсации масштабирования
    inv_trans = trans.inverse
    
    # 2. Получаем мировые оси, преобразованные в локальные координаты определения
    world_axes = [
      Geom::Vector3d.new(1, 0, 0),
      Geom::Vector3d.new(0, 1, 0), 
      Geom::Vector3d.new(0, 0, 1)
    ]
    
    # 3. Преобразуем нормаль в мировые координаты
    world_normal = normal_axis.transform(trans)
    
    # 4. Находим две оси, ортогональные нормали (в мировых координатах)
    world_axes_in_plane = world_axes.reject { |axis| axis.parallel?(world_normal) }
    
    if world_axes_in_plane.length < 2
      # Если не можем найти оси, используем альтернативный метод
      world_axes_in_plane = [
        world_normal.axes[0],
        world_normal.axes[1]
      ]
    end
    
    axis1 = world_axes_in_plane[0]
    axis2 = world_axes_in_plane[1]
    
    # 5. Преобразуем оси обратно в локальные координаты определения
    local_axis1 = axis1.transform(inv_trans)
    local_axis2 = axis2.transform(inv_trans)
    
    # 6. Нормализуем оси
    local_axis1.normalize!
    local_axis2.normalize!
    
    # 7. Убеждаемся, что оси ортогональны
    if local_axis1.dot(local_axis2) > 0.001
      # Делаем axis2 ортогональным axis1
      local_axis2 = local_axis2 - local_axis1 * local_axis1.dot(local_axis2)
      local_axis2.normalize!
    end
    
    # 8. Создаем круг с использованием add_circle (который Sketchup корректно масштабирует)
    # Вместо попытки компенсировать масштабирование, мы рисуем круг в определении,
    # и Sketchup сам правильно масштабирует его при любом трансформировании экземпляра
    circle = defn_entities.add_circle(center_pt, normal_axis, radius)
    circle.each { |e| e.set_attribute('ABFDrill', 'Type', type_tag) }
    
    # 9. Добавляем CPoint
    cpoint = defn_entities.add_cpoint(center_pt)
    cpoint.set_attribute('ABFDrill', 'Type', type_tag)
    
    return true
  end

  # --- ГЛАВНЫЙ ПРОЦЕСС ДЛЯ ПЕТЕЛЬ ---
  def self.process_hinge_holes(count, diameter_mm, length_offset_mm, side_offset_mm, screw_diameter_mm, screw_spacing_mm, screw_vert_offset_mm)
    model = Sketchup.active_model
    selection = model.selection
    items = selection.grep(Sketchup::ComponentInstance) + selection.grep(Sketchup::Group)

    if items.empty?
      UI.messagebox("Выберите один или несколько компонентов для установки петель.")
      return
    end

    model.start_operation("ABF Style Add Hinges", true)
    # Очищаем только петли на выделенных компонентах
    items.each { |ent| self.cleanup_hinge_drill_geometry(ent) }

    items.each do |entity|
      self.apply_hinge_holes(entity, count.to_i, diameter_mm.mm, length_offset_mm.mm, side_offset_mm.mm, screw_diameter_mm.mm, screw_spacing_mm.mm, screw_vert_offset_mm.mm)
    end

    model.commit_operation
  rescue => e
    UI.messagebox("Ошибка при создании петель: #{e.message}")
    model.abort_operation
  end

  # --- ЛОГИКА ДЛЯ СОЗДАНИЯ ОТВЕРСТИЙ ПОД ПЕТЛИ ---
  def self.apply_hinge_holes(entity, count, diameter, length_offset, side_offset, screw_diameter, screw_spacing, screw_vert_offset)
    defn = entity.respond_to?(:definition) ? entity.definition : entity.entities.parent
    bounds = defn.bounds

    # 1. Определяем оси компонента в локальных координатах
    dims = [bounds.width, bounds.height, bounds.depth]

    # Ось толщины (самая короткая) - Нормаль круга
    min_dim = dims.min
    thick_axis_idx = dims.index(min_dim)
    thick_axis = Geom::Vector3d.new(0,0,0)
    thick_axis[thick_axis_idx] = 1.0

    # Ось длины (самая длинная) - Ось для распределения петель
    max_dim = dims.max
    long_axis_idx = dims.index(max_dim)
    long_axis = Geom::Vector3d.new(0,0,0)
    long_axis[long_axis_idx] = 1.0

    # Ось ширины (средняя) - Ось для смещения 21.5мм и смещения саморезов
    mid_dim = dims.sort[1]
    mid_axis_idx = dims.index(mid_dim)
    mid_axis = Geom::Vector3d.new(0,0,0)
    mid_axis[mid_axis_idx] = 1.0

    # 2. Определяем плоскость для отверстий (по центру толщины)
    center_point = bounds.center.clone

    # 3. Определяем начальную точку и направление (Вдоль длинной оси)
    # Начальная координата для смещения по длине
    start_coord_length = bounds.min[long_axis_idx]
    offset_vec_length = long_axis.clone
    offset_vec_length.length = length_offset

    start_point_length = center_point.clone
    start_point_length[long_axis_idx] = start_coord_length
    start_point_length = start_point_length + offset_vec_length

    # 4. Рассчитываем точки для всех петель (Вдоль длинной оси)
    hole_points_length = []
    length = max_dim
    count = [1, count].max

    if count == 1
      hole_points_length << center_point
    else
      # Расстояние между центрами петель
      remaining_length = length - (length_offset * 2)
      if remaining_length < 0.mm
        hole_points_length << center_point
      else
        spacing = remaining_length / (count - 1).to_f
        (0...count).each do |i|
          current_point = start_point_length + long_axis.clone.tap { |v| v.length = i * spacing }
          hole_points_length << current_point
        end
      end
    end

    # 5. Применяем смещение по боковой оси (side_offset)
    # Начальная координата по средней оси (от которой отсчитываем 21.5мм)
    start_coord_side = bounds.min[mid_axis_idx]

    # Вектор смещения по средней оси
    side_offset_vec = mid_axis.clone
    side_offset_vec.length = side_offset

    # Точка, смещенная от края по средней оси
    side_offset_point = center_point.clone
    side_offset_point[mid_axis_idx] = start_coord_side
    side_offset_point = side_offset_point + side_offset_vec

    # Координата поверхности (max координата по оси толщины)
    surface_coord = bounds.max[thick_axis_idx]

    hole_points_length.each do |local_pt_length|
      final_pt = local_pt_length.clone

      # Устанавливаем координату по средней оси (смещение 21.5мм)
      final_pt[mid_axis_idx] = side_offset_point[mid_axis_idx]

      # Устанавливаем координату по оси толщины (на поверхность)
      final_pt[thick_axis_idx] = surface_coord

      # 1. Создаем отверстие под чашку (круг 35мм) - ИСПОЛЬЗУЕМ КОМПЕНСАЦИЮ
      self.draw_drill_hole_geometry(entity, final_pt, diameter / 2.0, thick_axis, 'HingeHole')

      # --- ЛОГИКА ДЛЯ ОТВЕРСТИЙ ПОД САМОРЕЗЫ ---
      half_spacing = screw_spacing / 2.0

      # Вектор смещения "выше чашки" (вдоль mid_axis)
      shift_vec = mid_axis.clone
      shift_vec.length = screw_vert_offset

      # Базовые точки вдоль длинной оси (long_axis), центрированные на final_pt
      pt1_base = final_pt - long_axis.clone.tap { |v| v.length = half_spacing }
      pt2_base = final_pt + long_axis.clone.tap { |v| v.length = half_spacing }

      # Применяем вертикальное смещение (shift_vec) к обеим точкам
      pt1 = pt1_base + shift_vec
      pt2 = pt2_base + shift_vec

      [pt1, pt2].each do |screw_pt|
        # Создаем отверстие под саморез (круг) - ИСПОЛЬЗУЕМ КОМПЕНСАЦИЮ
        self.draw_drill_hole_geometry(entity, screw_pt, screw_diameter / 2.0, thick_axis, 'HingeHole')
      end
      # --- КОНЕЦ ЛОГИКИ САМОРЕЗОВ ---
    end
  end

  # --- НОВЫЙ ГЛАВНЫЙ ПРОЦЕСС ДЛЯ ПЛАНКИ (ОБНОВЛЕНО) ---
  # Изменено: hinge_count и hinge_offset_mm заменены на count и length_offset_mm
  def self.process_strip_holes(diameter_mm, spacing_mm, side_offset_mm, count, length_offset_mm)
    model = Sketchup.active_model
    selection = model.selection
    items = selection.grep(Sketchup::ComponentInstance) + selection.grep(Sketchup::Group)

    if items.length != 1
      UI.messagebox("Выберите ОДИН компонент для установки отверстий под планку.")
      # Очищаем на всех выделенных, если их больше одного, или если ничего не выбрано
      model.start_operation("ABF Strip Drill Cleanup", true)
      items.each { |ent| self.cleanup_strip_drill_geometry(ent) }
      model.commit_operation
      return
    end

    entity = items.first

    model.start_operation("ABF Style Add Strip Holes", true)
    self.cleanup_strip_drill_geometry(entity) # Очистка перед созданием

    # Передаем параметры для позиционирования
    self.apply_strip_holes(entity, diameter_mm.mm, spacing_mm.mm, side_offset_mm.mm, count.to_i, length_offset_mm.mm)

    model.commit_operation
  rescue => e
    UI.messagebox("Ошибка при создании отверстий под планку: #{e.message}")
    model.abort_operation
  end

  # --- НОВАЯ ЛОГИКА ДЛЯ СОЗДАНИЯ ОТВЕРСТИЙ ПОД ПЛАНКУ (ОБНОВЛЕНО) ---
  # Изменено: hinge_count и hinge_offset заменены на count и length_offset
  def self.apply_strip_holes(entity, diameter, spacing, side_offset, count, length_offset)
    defn = entity.respond_to?(:definition) ? entity.definition : entity.entities.parent
    bounds = defn.bounds

    # 1. Определяем оси компонента в локальных координатах
    dims = [bounds.width, bounds.height, bounds.depth]

    # Ось толщины (самая короткая) - Ось сверления (Normal)
    min_dim = dims.min
    thick_axis_idx = dims.index(min_dim)
    thick_axis = Geom::Vector3d.new(0,0,0)
    thick_axis[thick_axis_idx] = 1.0

    # Ось длины (самая длинная) - Ось для распределения петель/планок
    max_dim = dims.max
    long_axis_idx = dims.index(max_dim)
    long_axis = Geom::Vector3d.new(0,0,0)
    long_axis[long_axis_idx] = 1.0

    # Ось ширины (средняя) - Ось для смещения от края (Side Offset)
    mid_dim = dims.sort[1]
    mid_axis_idx = dims.index(mid_dim)
    mid_axis = Geom::Vector3d.new(0,0,0)
    mid_axis[mid_axis_idx] = 1.0

    # 2. Расчет позиций центров планок вдоль длинной оси (long_axis)
    center_point = bounds.center.clone
    length = max_dim
    count = [1, count].max

    hole_points_length = [] # Это будут центры пар отверстий планки

    if count == 1
      # Если одна планка, то центр планки совпадает с центром компонента по длине
      hole_points_length << center_point
    else
      # Начальная координата для смещения по длине (от короткого края)
      start_coord_length = bounds.min[long_axis_idx]
      offset_vec_length = long_axis.clone
      offset_vec_length.length = length_offset # Используем новое смещение

      start_point_length = center_point.clone
      start_point_length[long_axis_idx] = start_coord_length
      start_point_length = start_point_length + offset_vec_length

      # Расстояние между центрами планок
      remaining_length = length - (length_offset * 2) # Используем новое смещение
      if remaining_length < 0.mm
        hole_points_length << center_point
      else
        spacing_strip = remaining_length / (count - 1).to_f
        (0...count).each do |i|
          # current_point - это центр пары планки по длинной оси
          current_point = start_point_length + long_axis.clone.tap { |v| v.length = i * spacing_strip }
          hole_points_length << current_point
        end
      end
    end

    # 3. Определяем координаты для смещения 37мм (Side Offset)
    # Начальная координата по средней оси (от которой отсчитываем 37мм)
    start_coord_side = bounds.min[mid_axis_idx]

    # Координата центра отверстий по средней оси (37мм от края)
    side_coord = start_coord_side + side_offset

    # 4. Определяем координату поверхности (по оси толщины)
    # Отверстия сверлятся в пласть, поэтому берем max координату по оси толщины
    surface_coord = bounds.max[thick_axis_idx]

    # 5. Создаем точки и отверстия
    half_spacing = spacing / 2.0 # Половина межосевого расстояния планки (32/2 = 16мм)

    # hole_points_length - это центры пар планок
    hole_points_length.each do |center_pt_long_axis|
      # Координата центра пары отверстий по длинной оси
      center_coord_length = center_pt_long_axis[long_axis_idx]

      # --- НОВОЕ: Создание точки в центре между отверстиями планки ---
      final_center_pt = Geom::Point3d.new(0, 0, 0)
      final_center_pt[long_axis_idx] = center_coord_length
      final_center_pt[mid_axis_idx] = side_coord
      final_center_pt[thick_axis_idx] = surface_coord

      cpoint = defn.entities.add_cpoint(final_center_pt)
      cpoint.set_attribute('ABFDrill', 'Type', 'StripHole')
      # --- КОНЕЦ НОВОГО БЛОКА ---

      # Координаты центров отверстий по длинной оси (смещение +/- 16мм)
      length_coord_1 = center_coord_length - half_spacing
      length_coord_2 = center_coord_length + half_spacing

      hole_coords_length = [length_coord_1, length_coord_2]

      # Создаем пару отверстий
      hole_coords_length.each do |length_coord|
        final_pt = Geom::Point3d.new(0, 0, 0)

        # Устанавливаем координаты
        final_pt[long_axis_idx] = length_coord # Смещение +/- 16мм от центра
        final_pt[mid_axis_idx] = side_coord    # Смещение 37мм от края
        final_pt[thick_axis_idx] = surface_coord # На поверхности

        # Создаем отверстие - ИСПОЛЬЗУЕМ КОМПЕНСАЦИЮ
        self.draw_drill_hole_geometry(entity, final_pt, diameter / 2.0, thick_axis, 'StripHole')
      end
    end
  end

  # --- МЕТОД: Аннотации (обновлен для включения StripHole) ---
  def self.show_drill_annotations
    model = Sketchup.active_model
    entities = model.active_entities
    selection = model.selection

    model.start_operation('Проставить/Удалить Аннотации Присадок', true)

    if @annotations_visible
      self.cleanup_annotations(entities)
      @annotations_visible = false
      model.commit_operation
      UI.messagebox("Аннотации размеров удалены.")
      return
    end

    items = selection.grep(Sketchup::ComponentInstance) + selection.grep(Sketchup::Group)
    if items.empty?
      UI.messagebox("Выберите компоненты.")
      model.abort_operation
      return
    end

    self.cleanup_annotations(entities)
    base_offset_distance = 50.mm
    step_size = 50.mm

    items.each do |entity|
      bb = entity.bounds
      bb_center = bb.center
      defn_entities = entity.respond_to?(:definition) ? entity.definition.entities : entity.entities

      # Общие размеры
      overall_offset_y = -350.mm
      start_pt_x = Geom::Point3d.new(bb.min.x, bb.min.y, bb.min.z)
      end_pt_x = Geom::Point3d.new(bb.max.x, bb.min.y, bb.min.z)
      offset_x = Geom::Vector3d.new(0, overall_offset_y, 0)
      dim_overall_x = entities.add_dimension_linear(start_pt_x, end_pt_x, offset_x)
      dim_overall_x.set_attribute('ABFDrill', 'Type', 'Dimension')

      overall_offset_x = -350.mm
      start_pt_y = Geom::Point3d.new(bb.min.x, bb.min.y, bb.min.z)
      end_pt_y = Geom::Point3d.new(bb.min.x, bb.max.y, bb.min.z)
      offset_y = Geom::Vector3d.new(overall_offset_x, 0, 0)
      dim_overall_y = entities.add_dimension_linear(start_pt_y, end_pt_y, offset_y)
      dim_overall_y.set_attribute('ABFDrill', 'Type', 'Dimension')

      # Размеры до отверстий
      # Собираем все точки присадки, петель и планки
      drill_cpoints = defn_entities.grep(Sketchup::ConstructionPoint).select do |e|
        type = e.get_attribute('ABFDrill', 'Type')
        type == 'DrillHole' || type == 'HingeHole' || type == 'StripHole'
      end

      dimensions_to_place = []

      drill_cpoints.each do |cpoint|
        hole_pt = cpoint.position.transform(entity.transformation)

        # X Dimension
        dist_from_min_x = hole_pt.x - bb.min.x
        dist_from_max_x = bb.max.x - hole_pt.x
        if dist_from_min_x <= dist_from_max_x
          start_pt_x = Geom::Point3d.new(bb.min.x, hole_pt.y, hole_pt.z)
          value_x = dist_from_min_x
        else
          start_pt_x = Geom::Point3d.new(bb.max.x, hole_pt.y, hole_pt.z)
          value_x = dist_from_max_x
        end
        direction_x = (hole_pt.y > bb_center.y) ? :Up : :Down

        if value_x.abs > 0.001.mm
          dimensions_to_place << { :type => :X, :value => value_x, :start_pt => start_pt_x, :end_pt => hole_pt, :direction => direction_x }
        end

        # Y Dimension
        dist_from_min_y = hole_pt.y - bb.min.y
        dist_from_max_y = bb.max.y - hole_pt.y
        if dist_from_min_y <= dist_from_max_y
          start_pt_y = Geom::Point3d.new(hole_pt.x, bb.min.y, hole_pt.z)
          value_y = dist_from_min_y
        else
          start_pt_y = Geom::Point3d.new(hole_pt.x, bb.max.y, hole_pt.z)
          value_y = dist_from_max_y
        end
        direction_y = (hole_pt.x > bb_center.x) ? :Right : :Left

        if value_y.abs > 0.001.mm
          dimensions_to_place << { :type => :Y, :value => value_y, :start_pt => start_pt_y, :end_pt => hole_pt, :direction => direction_y }
        end
      end

      grouped_dims = dimensions_to_place.group_by { |d| d[:direction] }
      step_counters = { :Up => 0, :Down => 0, :Right => 0, :Left => 0 }

      grouped_dims.each do |direction, dims|
        dims.sort_by! { |d| d[:value] }
        dims.each do |d|
          step = step_counters[direction]
          offset_val = base_offset_distance + step * step_size
          offset_vec = case direction
                       when :Up    then Geom::Vector3d.new(0, offset_val, 0)
                       when :Down  then Geom::Vector3d.new(0, -offset_val, 0)
                       when :Right then Geom::Vector3d.new(offset_val, 0, 0)
                       when :Left  then Geom::Vector3d.new(-offset_val, 0, 0)
                       end
          dim = entities.add_dimension_linear(d[:start_pt], d[:end_pt], offset_vec)
          dim.set_attribute('ABFDrill', 'Type', 'Dimension')
          step_counters[direction] += 1
        end
      end
    end

    @annotations_visible = true
    model.commit_operation
    UI.messagebox("Аннотации проставлены.")
  rescue => e
    UI.messagebox("Ошибка: #{e.message}")
    model.abort_operation
  end

  # --- МЕТОД: Раскладка (без изменений) ---
  def self.line_up_components
    model = Sketchup.active_model
    selection = model.selection
    entities_to_line_up = selection.grep(Sketchup::ComponentInstance) + selection.grep(Sketchup::Group)

    if entities_to_line_up.empty?
      UI.messagebox("Выберите компоненты.")
      return
    end

    model.start_operation('Разложить в линию', true)
    begin
      self.cleanup_annotations(model.active_entities)
      @annotations_visible = false

      entities_to_line_up.sort_by! do |e|
        b = e.bounds
        [b.width, b.height, b.depth].max
      end

      cursor_x = 0.0
      gap = GAP

      entities_to_line_up.each do |entity|
        # Сброс трансформации (включая масштабирование) - это гарантирует,
        # что круги, нарисованные в определении, будут выглядеть как круги.
        entity.transformation = Geom::Transformation.new

        bounds = entity.bounds
        dims = [bounds.width, bounds.height, bounds.depth]
        smallest_idx = dims.each_with_index.min[1]

        rot_flat = Geom::Transformation.new
        if smallest_idx == 0
          rot_flat = Geom::Transformation.rotation(bounds.center, Geom::Vector3d.new(0, 1, 0), -90.degrees)
        elsif smallest_idx == 1
          rot_flat = Geom::Transformation.rotation(bounds.center, Geom::Vector3d.new(1, 0, 0), 90.degrees)
        end
        entity.transform!(rot_flat)

        bounds = entity.bounds
        rot_orient = Geom::Transformation.new
        if bounds.width < bounds.height
          rot_orient = Geom::Transformation.rotation(bounds.center, Geom::Vector3d.new(0, 0, 1), 90.degrees)
        end
        entity.transform!(rot_orient)

        bounds = entity.bounds
        move_vec = Geom::Vector3d.new(cursor_x - bounds.min.x, -bounds.min.y, -bounds.min.z)
        entity.transform!(Geom::Transformation.translation(move_vec))

        cursor_x = entity.bounds.max.x + gap
      end
      model.active_view.zoom(entities_to_line_up)
    rescue => e
      UI.messagebox("Ошибка: #{e.message}")
    ensure
      model.commit_operation
    end
  end

  unless file_loaded?(__FILE__)
    menu = UI.menu('Plugins')
    menu.add_item('ABF Style Drill & Hinges') { self.show_dialog }
    file_loaded(__FILE__)
  end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants