Last active
February 19, 2026 14:18
-
-
Save AlexandrDragunkin/c0792643bc96e0ac60929eab2941d3fc to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # -*- coding: utf-8 -*- | |
| from __future__ import annotations | |
| __author__ = 'Aleksandr Dragunkin --' | |
| __created__ = '23.01.2026' | |
| __version__ = '0.0.01' | |
| # ------------------------------------------------------------------------------- | |
| # Name: stl2wrl | |
| # Purpose: Преобразование STL файла в VRML/WRL файл в среде к3мебель. | |
| # Copyright: (c) GEOS 2012-2025 http://k3info.ru/ | |
| # Licence: FREE | |
| # ------------------------------------------------------------------------------ | |
| # Преобразует STL файл в VRML/WRL файл | |
| # NOTE: '26.01.2026' (ДАР) зависимость (VENV37) pip install numpy-stl==3.1.2 -U --target <UserProto>/site-packages | |
| # | |
| # Избегает повторного создания точек, если точки грани перекрываются на модели. | |
| # Цветовая модель серая | |
| # Bradley Boccuzzi 2020-2025 | |
| import argparse | |
| import os | |
| from collections import OrderedDict | |
| try: | |
| SHEMA_K3 = True | |
| import k3 | |
| from k3_widgets import setvar | |
| except ImportError: | |
| SHEMA_K3 = False | |
| pass | |
| print( | |
| "Не удалось импортировать модуль k3. Значит работаем в стандартной схеме Bradley Boccuzzi через командную строку терминала" | |
| ) | |
| try: | |
| import stl | |
| from stl import mesh | |
| import numpy | |
| NUMPY_STL_AVAILABLE = True | |
| except ImportError: | |
| NUMPY_STL_AVAILABLE = False | |
| print( | |
| "Не удалось импортировать numpy-stl. Преобразование бинарных STL файлов не будет доступно." | |
| ) | |
| def convert(stl_dir: str, wrl_dir: str, sf: float) -> None: | |
| """ | |
| Преобразует STL файл в WRL файл. | |
| Аргументы: | |
| stl_dir (str): Путь к входному STL файлу | |
| wrl_dir (str): Путь к выходному WRL файлу | |
| sf (float): Коэффициент масштабирования | |
| """ | |
| # Проверить, что stl_path является полным путем к реальному STL файлу в ASCII символах | |
| validate_stl_path(stl_dir) | |
| # Открыть нужный STL файл | |
| stl_fd = open(stl_dir, 'r', encoding='UTF-8') | |
| wrl_fd = open(wrl_dir, 'w', encoding='UTF-8') | |
| # Создать и заполнить объект модели | |
| temp_model = Model() | |
| parse_stl(stl_fd, temp_model, sf) | |
| vrml = generate_vrml(temp_model) | |
| wrl_fd.write(vrml) | |
| wrl_fd.flush() | |
| wrl_fd.close() | |
| # Закрыть дескриптор входного файла STL | |
| stl_fd.close() | |
| def parse_stl(file: object, model: Model, sf: float) -> None: | |
| """ | |
| Парсит STL файл и заполняет объект модели. | |
| Аргументы: | |
| file (object): Дескриптор входного файла | |
| model (Model): Объект модели для заполнения | |
| sf (float): Коэффициент масштабирования | |
| """ | |
| # Необходимо преобразовать мм в дюймы | |
| _scaling_factor = float(sf) | |
| # Быстрый и грязный парсинг | |
| for line in file: | |
| # solid<*whitespace>(name) | |
| if "solid " in line: | |
| model_name = line.split()[1] | |
| model.set_name(model_name) | |
| # Определить грань | |
| if "facet normal" in line: | |
| # Получить нормаль поверхности грани | |
| surface_normal = line.split()[2:5] | |
| normal_x = surface_normal[0] | |
| normal_y = surface_normal[1] | |
| normal_z = surface_normal[2] | |
| temp_normal = Vertex(normal_x, normal_y, normal_z) | |
| # Очистить строку 'outer loop' с помощью этого оператора readline ниже | |
| line = file.readline() | |
| # Получить отдельную вершину из файла | |
| temp_vertex = [] | |
| for i in range(3): | |
| line = file.readline() | |
| # Начать цикл треугольника | |
| # if "outer loop" in line: | |
| if "vertex" in line: | |
| points = line.split()[1:4] | |
| # TODO - Сколько знаков после запятой допустимо для VRML? | |
| # "SFFloats and MFFloats are written to the VRML file in ISO C floating point format" | |
| # - https://www.web3d.org/documents/specifications/14772/V2.0/part1/fieldsRef.html | |
| # TODO - Коэффициент масштабирования командной строки | |
| point_x = '%0.7f' % (float(points[0]) / _scaling_factor) | |
| point_y = '%0.7f' % (float(points[1]) / _scaling_factor) | |
| point_z = '%0.7f' % (float(points[2]) / _scaling_factor) | |
| vertex = Vertex(point_x, point_y, point_z) | |
| temp_vertex.append(vertex) | |
| # Создать отдельный треугольник для грани на основе трех полученных выше вершин | |
| temp_triangle = Triangle(temp_vertex[0], temp_vertex[1], temp_vertex[2]) | |
| # Создать грань из нормали поверхности и треугольника | |
| temp_facet = Facet(temp_triangle, temp_normal) | |
| # Добавить грань в модель | |
| model.add_facet(temp_facet) | |
| # Быстрый и грязный экспорт | |
| # Shape { | |
| # geometry IndexedFaceSet { | |
| # creaseAngle 0.50 coordIndex [index, index, index, index,...] | |
| # | |
| # coord Coordinate { | |
| # point [ | |
| # x y z, //index 0 | |
| # x y z, //index 1 | |
| # . . ., | |
| # x y z, //index N-1 | |
| # x y z //index N | |
| # ] | |
| # } | |
| # } | |
| # | |
| # свойства внешнего вида | |
| # | |
| # } | |
| # Default color of everything is gray | |
| _default_texture = "Shape {\n\tappearance Appearance {material DEF MAT Material {\n\t\tambientIntensity 0.45\n\t\tdiffuseColor 0.8 0.8 0.7\n\t\tspecularColor 0.19 0.28 0.3\n\t\temissiveColor 0.0 0.0 0.0\n\t\tshininess 0.85\n\t\ttransparency 0.0\n\t\t}\n\t}\n}" | |
| _string_template = "#VRML V2.0 utf8\n#Generated with stl2wrl - Bradley Boccuzzi 2020-2025\n\n<default_texture>\nShape {\n\tgeometry IndexedFaceSet {\n\t\tcreaseAngle 0.50 coordIndex [<index_list>]\n\t\tcoord Coordinate {\n\t\t\tpoint [\n<point_list>\n\t\t\t]\n\t\t}\n\t}\n\tappearance Appearance{material USE MAT}\n}" | |
| # Генерирует VRML файл мира из объекта модели | |
| def generate_vrml(model: Model) -> str: | |
| """ | |
| Генерирует VRML строку из объекта модели. | |
| Аргументы: | |
| model (Model): Объект модели | |
| Возвращает: | |
| str: Строка VRML | |
| """ | |
| # Индекс для каждой вершины | |
| index = 0 | |
| index_list = [] | |
| point_dict = OrderedDict() | |
| # Итерация по граням модели... | |
| for facet in model.facets: | |
| for vertex in facet.triangle.vertices: | |
| # Создать запись точки | |
| point = vertex.x + ' ' + vertex.y + ' ' + vertex.z | |
| # Добавить новую точку в point_dict, если она еще не существует | |
| if point not in point_dict: | |
| point_dict[point] = index | |
| index_list.append(str(index)) | |
| index += 1 | |
| # ... Иначе повторно использовать существующую точку на основе ее индекса | |
| else: | |
| index_list.append(str(point_dict[point])) | |
| # Конец грани | |
| index_list.append('-1') | |
| # Преобразовать списки в строки для генерации VRML файла | |
| index_str = ', '.join(index_list) | |
| # Создать список точек в порядке их добавления | |
| point_list = list(point_dict.keys()) | |
| point_str = ', '.join(point_list) | |
| # Выполнить подстановку в шаблоне | |
| return_string = ( | |
| _string_template.replace('<index_list>', index_str) | |
| .replace('<point_list>', point_str) | |
| .replace('<default_texture>', _default_texture) | |
| ) | |
| return return_string | |
| def validate_stl_path(stl_path: str) -> None: | |
| """ | |
| Проверяет, что stl_path является полным путем к реальному STL файлу в ASCII символах. | |
| Аргументы: | |
| stl_path (str): Путь к STL файлу | |
| Вызывает: | |
| ValueError: Если путь не является абсолютным или файл не является STL ASCII файлом | |
| FileNotFoundError: Если файл не существует | |
| """ | |
| # Проверить, что stl_path является полным путем | |
| if not os.path.isabs(stl_path): | |
| raise ValueError("stl_path должен быть полным (абсолютным) путем") | |
| # Проверить существование файла | |
| if not os.path.exists(stl_path): | |
| raise FileNotFoundError(f"STL файл не найден: {stl_path}") | |
| # Проверить, что файл является текстовым STL файлом (не бинарным) | |
| try: | |
| with open(stl_path, 'r', encoding='UTF-8') as test_fd: | |
| first_line = test_fd.readline() | |
| if not first_line.strip().startswith('solid'): | |
| raise ValueError("Файл не является корректным STL ASCII файлом") | |
| except UnicodeDecodeError: | |
| # Если файл бинарный и доступен numpy-stl, преобразуем его в ASCII | |
| if NUMPY_STL_AVAILABLE: | |
| try: | |
| # Загрузить бинарный STL файл | |
| binary_stl = mesh.Mesh.from_file(stl_path) | |
| # Сохранить как ASCII STL | |
| ascii_path = stl_path + '_ascii.stl' | |
| binary_stl.save(ascii_path, mode=stl.Mode.ASCII) | |
| # Заменить оригинальный файл на ASCII версию | |
| os.replace(ascii_path, stl_path) | |
| except Exception as e: | |
| raise ValueError(f"Не удалось преобразовать бинарный STL файл: {e}") | |
| else: | |
| raise UnicodeDecodeError( | |
| "Файл не является текстовым STL файлом (возможно бинарный)" | |
| ) | |
| # Вершина в 3-мерном пространстве | |
| class Vertex: | |
| def __init__(self, x: str, y: str, z: str) -> None: | |
| """ | |
| Инициализирует вершину в 3-мерном пространстве. | |
| Аргументы: | |
| x (str): Координата X | |
| y (str): Координата Y | |
| z (str): Координата Z | |
| """ | |
| self.x = x | |
| self.y = y | |
| self.z = z | |
| # Треугольник, состоящий из 3 вершин в 3-мерном пространстве | |
| class Triangle: | |
| def __init__(self, v1: Vertex, v2: Vertex, v3: Vertex) -> None: | |
| """ | |
| Инициализирует треугольник, состоящий из 3 вершин в 3-мерном пространстве. | |
| Аргументы: | |
| v1 (Vertex): Первая вершина | |
| v2 (Vertex): Вторая вершина | |
| v3 (Vertex): Третья вершина | |
| """ | |
| self.vertices = [v1, v2, v3] | |
| # Каждая грань содержит три вершины (треугольник) и нормаль поверхности | |
| class Facet: | |
| def __init__(self, triangle: Triangle, normal: Vertex) -> None: | |
| """ | |
| Инициализирует грань, содержащую три вершины (треугольник) и нормаль поверхности. | |
| Аргументы: | |
| triangle (Triangle): Треугольник | |
| normal (Vertex): Нормаль поверхности | |
| """ | |
| self.triangle = triangle | |
| self.normal = normal | |
| # Объект модели | |
| # Этот класс в целом отражает структуру STL файла, хотя может быть изменен позже... | |
| class Model: | |
| def __init__(self) -> None: | |
| """ | |
| Инициализирует объект модели. | |
| Этот класс в целом отражает структуру STL файла, хотя может быть изменен позже... | |
| """ | |
| # Инициализировать модель, состоящую из граней и т.д... | |
| self.name = "" | |
| self.facets = [] | |
| def add_facet(self, facet: Facet) -> None: | |
| """ | |
| Добавляет грань в модель. | |
| Аргументы: | |
| facet (Facet): Грань для добавления | |
| """ | |
| self.facets.append(facet) | |
| # TODO - должен ли это быть частью потока парсинга? | |
| def set_name(self, name: str) -> None: | |
| """ | |
| Устанавливает имя модели. | |
| Аргументы: | |
| name (str): Имя модели | |
| """ | |
| self.name = name | |
| def main(): | |
| # Проверить, запущен ли скрипт в среде к3мебель | |
| if SHEMA_K3: | |
| # Получить параметры из к3мебель | |
| params = k3.getpar() | |
| if len(params) > 0: | |
| stl_path = params[0].value | |
| # Проверить, есть ли второй параметр (коэффициент масштабирования) | |
| if len(params) > 1: | |
| sf = float(params[1].value) | |
| else: | |
| sf = 1.0 | |
| else: | |
| # print("Не указан путь к STL файлу") | |
| dlg = setvar.SetVar() | |
| dlg.promt = setvar.Title('STL2WRL', 'Параметры:') | |
| dlg.widgets.extend( | |
| [setvar.WFile('Путь к STL файлу'), | |
| setvar.WDigit('Масштаб', defval=1.0)] | |
| ) | |
| is_ok = dlg.view() | |
| if is_ok: | |
| stl_path = dlg.widgets[0].value | |
| sf = float(dlg.widgets[1].value) | |
| else: | |
| print('Операция отменена пользователем') | |
| return | |
| else: | |
| # Настройка парсинга аргументов CLI | |
| argparser = argparse.ArgumentParser() | |
| argparser.add_argument("stl") | |
| argparser.add_argument("scaling") | |
| args = argparser.parse_args() | |
| # Получить путь STL и создать имя файла WRL | |
| stl_path = args.stl | |
| # Получить коэффициент масштабирования | |
| sf = args.scaling | |
| try: | |
| # Проверить, что stl_path является полным путем к реальному STL файлу в ASCII символах | |
| validate_stl_path(stl_path) | |
| except ValueError as e: | |
| print(f"Файл {stl_path} не является STL файлом: {e}") | |
| return | |
| except UnicodeDecodeError: | |
| print(f"Файл {stl_path} не является текстовым STL файлом") | |
| return | |
| except FileNotFoundError: | |
| print(f"Файл {stl_path} не найден") | |
| return | |
| else: | |
| # Получить имя файла WRL | |
| wrl_name = os.path.basename(stl_path).split('.')[0] + '.wrl' | |
| wrl_path = os.path.dirname(stl_path) | |
| if wrl_path == "": | |
| wrl_path = wrl_name | |
| else: | |
| wrl_path = os.path.join(wrl_path, wrl_name) | |
| # Выполнить преобразование | |
| convert(stl_path, wrl_path, sf) | |
| print("Преобразование завершено!") | |
| # ГОТОВО | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment