Skip to content

Instantly share code, notes, and snippets.

@AlexandrDragunkin
Last active February 19, 2026 14:18
Show Gist options
  • Select an option

  • Save AlexandrDragunkin/c0792643bc96e0ac60929eab2941d3fc to your computer and use it in GitHub Desktop.

Select an option

Save AlexandrDragunkin/c0792643bc96e0ac60929eab2941d3fc to your computer and use it in GitHub Desktop.
# -*- 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