Realizamos un proyecto de complemento con compilación para diferentes versiones de Revit/AutoCAD

Realizamos un proyecto de complemento con compilación para diferentes versiones de Revit/AutoCAD

Al desarrollar complementos para aplicaciones CAD (en mi caso estos son AutoCAD, Revit y Renga) con el tiempo, aparece un problema: se lanzan nuevas versiones de los programas, se modifican sus API y es necesario realizar nuevas versiones de complementos.

Cuando solo tiene un complemento o aún es un principiante autodidacta en este tema, simplemente puede hacer una copia del proyecto, cambiar los lugares necesarios en él y ensamblar una nueva versión del complemento. En consecuencia, los cambios posteriores en el código implicarán un aumento múltiple de los costos laborales.

A medida que adquiera experiencia y conocimientos, encontrará varias formas de automatizar este proceso. Caminé por este camino y quiero contarles con qué terminé y lo conveniente que es.

Primero, veamos un método que es obvio y que he usado durante mucho tiempo.

Enlaces a archivos de proyecto

Y para que todo sea simple, visual y comprensible, lo describiré todo utilizando un ejemplo abstracto de desarrollo de complementos.

Abramos Visual Studio (tengo la versión Community 2019. Y sí, en ruso) y creemos una nueva solución. llamémoslo MiSuperPluginParaRevit

Realizamos un proyecto de complemento con compilación para diferentes versiones de Revit/AutoCAD

Crearemos un complemento para Revit para las versiones 2015-2020. Por lo tanto, creemos un nuevo proyecto en la solución (Biblioteca de clases Net Framework) y llamémoslo MiSuperPluginParaRevit_2015

Realizamos un proyecto de complemento con compilación para diferentes versiones de Revit/AutoCAD

Necesitamos agregar enlaces a la API de Revit. Por supuesto, podemos agregar enlaces a archivos locales (necesitaremos instalar todos los SDK necesarios o todas las versiones de Revit), pero inmediatamente seguiremos el camino correcto y conectaremos el paquete NuGet. Puedes encontrar bastantes paquetes, pero usaré el mío.

Después de conectar el paquete, haga clic derecho en el elemento "referencias" y seleccione el elemento "Mueva paquetes.config a PackageReference...»

Realizamos un proyecto de complemento con compilación para diferentes versiones de Revit/AutoCAD

Si de repente en este punto empiezas a entrar en pánico, porque en la ventana de propiedades del paquete no habrá ningún elemento importante "Copiar localmente", que definitivamente debemos establecer en el valor false, entonces no entre en pánico: vaya a la carpeta con el proyecto, abra el archivo con la extensión .csproj en un editor que le resulte conveniente (yo uso Notepad++) y busque allí una entrada sobre nuestro paquete. Ella se ve así ahora:

<PackageReference Include="ModPlus.Revit.API.2015">
  <Version>1.0.0</Version>
</PackageReference>

Agregarle una propiedad tiempo de ejecución. Resultará así:

<PackageReference Include="ModPlus.Revit.API.2015">
  <Version>1.0.0</Version>
  <ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>

Ahora, al crear un proyecto, los archivos del paquete no se copiarán a la carpeta de salida.
Vayamos más allá: imaginemos inmediatamente que nuestro complemento utilizará algo de la API de Revit, que ha cambiado con el tiempo cuando se lanzaron nuevas versiones. Bueno, o simplemente necesitamos cambiar algo en el código dependiendo de la versión de Revit para la que estemos creando el complemento. Para resolver tales diferencias en el código, usaremos símbolos de compilación condicional. Abra las propiedades del proyecto, vaya a la pestaña "asamblea"y en el campo"Notación de compilación condicional"vamos a escribir R2015.

Realizamos un proyecto de complemento con compilación para diferentes versiones de Revit/AutoCAD

Tenga en cuenta que el símbolo debe agregarse para las configuraciones de depuración y versión.

Bueno, ya que estamos en la ventana de propiedades, inmediatamente nos dirigimos a la pestaña “solicitud"y en el campo"Espacio de nombres predeterminado» eliminar el sufijo _2015para que nuestro espacio de nombres sea universal e independiente del nombre del ensamblado:

Realizamos un proyecto de complemento con compilación para diferentes versiones de Revit/AutoCAD

En mi caso, en el producto final, los complementos de todas las versiones se colocan en una carpeta, por lo que los nombres de mis ensamblados permanecen con el sufijo del formulario. _20хх. Pero también puede eliminar el sufijo del nombre del ensamblaje si se supone que los archivos deben estar ubicados en carpetas diferentes.

Vayamos al código del archivo. Clase1.cs y simular algo de código allí, teniendo en cuenta diferentes versiones de Revit:

namespace MySuperPluginForRevit
{
    using Autodesk.Revit.Attributes;
    using Autodesk.Revit.DB;
    using Autodesk.Revit.UI;

    [Regeneration(RegenerationOption.Manual)]
    [Transaction(TransactionMode.Manual)]
    public class Class1 : IExternalCommand
    {
        public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
        {
#if R2015
            TaskDialog.Show("ModPlus", "Hello Revit 2015");
#elif R2016
            TaskDialog.Show("ModPlus", "Hello Revit 2016");
#elif R2017
            TaskDialog.Show("ModPlus", "Hello Revit 2017");
#elif R2018
            TaskDialog.Show("ModPlus", "Hello Revit 2018");
#elif R2019
            TaskDialog.Show("ModPlus", "Hello Revit 2019");
#elif R2020
            TaskDialog.Show("ModPlus", "Hello Revit 2020");
#endif
            return Result.Succeeded;
        }
    }
}

Inmediatamente tomé en cuenta todas las versiones de Revit superiores a la versión 2015 (que estaban disponibles en el momento de escribir este artículo) e inmediatamente tomé en cuenta la presencia de símbolos de compilación condicionales, que se crean utilizando la misma plantilla.

Pasemos al punto culminante principal. Creamos un nuevo proyecto en nuestra solución, solo para la versión del complemento para Revit 2016. Repetimos todos los pasos descritos anteriormente, respectivamente, reemplazando el número 2015 por el número 2016. Pero el archivo Clase1.cs eliminar del nuevo proyecto.

Realizamos un proyecto de complemento con compilación para diferentes versiones de Revit/AutoCAD

Archivo con el código requerido - Clase1.cs – ya lo tenemos y solo necesitamos insertar un enlace en un nuevo proyecto. Hay dos formas de insertar enlaces:

  1. Largo – haga clic derecho en el proyecto y seleccione “Añadir"->"Elemento existente", en la ventana que se abre, busque el archivo requerido y en lugar de la opción "Añadir"seleccione la opción"Agregar como conexión»

Realizamos un proyecto de complemento con compilación para diferentes versiones de Revit/AutoCAD

  1. corto – directamente en el explorador de soluciones, seleccione el archivo deseado (o incluso archivos, o incluso carpetas enteras) y arrástrelo a un nuevo proyecto mientras mantiene presionada la tecla Alt. Mientras arrastra, verá que cuando presione la tecla Alt, el cursor del mouse cambiará de un signo más a una flecha.
    UPD: Cometí un poco de confusión en este párrafo: para transferir varios archivos debes mantener presionado Mayús + Alt!

Luego de realizar el trámite, ya tendremos un archivo en el segundo proyecto. Clase1.cs con el icono correspondiente (flecha azul):

Realizamos un proyecto de complemento con compilación para diferentes versiones de Revit/AutoCAD

Al editar código en la ventana del editor, también puede elegir en qué contexto del proyecto mostrar el código, lo que le permitirá ver el código que se está editando bajo diferentes símbolos de compilación condicional:

Realizamos un proyecto de complemento con compilación para diferentes versiones de Revit/AutoCAD

Creamos todos los demás proyectos (2017-2020) utilizando este esquema. Truco de vida: si arrastra archivos en el Explorador de soluciones no desde el proyecto base, sino desde el proyecto donde ya están insertados como un enlace, ¡no tendrá que mantener presionada la tecla Alt!

La opción descrita es bastante buena hasta el momento de agregar una nueva versión del complemento o hasta el momento de agregar nuevos archivos al proyecto; todo esto se vuelve muy tedioso. Y recientemente, de repente me di cuenta de cómo solucionarlo todo con un proyecto y pasamos al segundo método.

La magia de las configuraciones.

Habiendo terminado de leer aquí, puede exclamar: "¿Por qué describiste el primer método, si el artículo trata inmediatamente sobre el segundo?" Y describí todo para que quede más claro por qué necesitamos símbolos de compilación condicional y en qué lugares difieren nuestros proyectos. Y ahora nos queda más claro exactamente qué diferencias en los proyectos debemos implementar, dejando solo un proyecto.

Y para que todo sea más obvio, no crearemos un nuevo proyecto, sino que haremos cambios en nuestro proyecto actual creado de la primera forma.

Entonces, antes que nada, eliminamos todos los proyectos de la solución excepto el principal (que contiene los archivos directamente). Aquellos. proyectos para las versiones 2016-2020. Abra la carpeta con la solución y elimine las carpetas de estos proyectos allí.

Nos queda un proyecto en nuestra decisión: MiSuperPluginParaRevit_2015. Abra sus propiedades y:

  1. En la pestaña "solicitud"eliminar el sufijo del nombre del ensamblado _2015 (quedará claro por qué más adelante)
  2. En la pestaña "asamblea» eliminar el símbolo de compilación condicional R2015 del campo correspondiente

Nota: la última versión de Visual Studio tiene un error: los símbolos de compilación condicional no se muestran en la ventana de propiedades del proyecto, aunque están disponibles. Si experimenta este problema, deberá eliminarlo manualmente del archivo .csproj. Sin embargo, todavía tenemos que trabajar en ello, así que sigue leyendo.

Cambie el nombre del proyecto en la ventana del Explorador de soluciones eliminando el sufijo _2015 y luego elimine el proyecto de la solución. ¡Esto es necesario para mantener el orden y los sentimientos de los perfeccionistas! Abrimos la carpeta de nuestra solución, cambiamos el nombre de la carpeta del proyecto allí de la misma manera y cargamos el proyecto nuevamente en la solución.

Abra el administrador de configuración. configuración de EE. UU. tortugitas en principio no será necesario, por lo que lo eliminamos. Creamos nuevas configuraciones con nombres que ya nos resultan familiares R2015, R2016, ..., R2020. Tenga en cuenta que no necesita copiar configuraciones de otras configuraciones y no necesita crear configuraciones de proyecto:

Realizamos un proyecto de complemento con compilación para diferentes versiones de Revit/AutoCAD

Vaya a la carpeta con el proyecto y abra el archivo con la extensión .csproj en un editor que le resulte conveniente. Por cierto, también puede abrirlo en Visual Studio; debe descargar el proyecto y luego el elemento deseado estará en el menú contextual:

Realizamos un proyecto de complemento con compilación para diferentes versiones de Revit/AutoCAD

Incluso es preferible editar en Visual Studio, ya que el editor alinea y solicita.

En el archivo veremos los elementos. grupo de propiedades – en lo más alto está el general, y luego vienen las condiciones. Estos elementos establecen las propiedades del proyecto cuando se construye. El primer elemento, que no tiene condiciones, establece propiedades generales y, en consecuencia, los elementos con condiciones cambian algunas propiedades según las configuraciones.

Ir al elemento común (primer) grupo de propiedades y mira la propiedad Nombre de ensamblaje – este es el nombre del ensamblaje y deberíamos tenerlo sin sufijo _2015. Si hay un sufijo, elimínelo.

Encontrar un elemento con una condición.

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">

No lo necesitamos, lo eliminamos.

Elemento con condición

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">

Será necesario trabajar en la etapa de desarrollo y depuración del código. Puede cambiar sus propiedades para adaptarlas a sus necesidades: establecer diferentes rutas de salida, cambiar símbolos de compilación condicionales, etc.

Ahora creemos nuevos elementos. grupo de propiedades para nuestras configuraciones. En estos elementos sólo necesitamos establecer cuatro propiedades:

  • Ruta de salida – carpeta de salida. Establecí el valor predeterminado binR20xx
  • Definir constantes – símbolos de compilación condicional. Se debe especificar el valor TRAZA;R20хх
  • Versión del marco de destino – versión de plataforma. Las diferentes versiones de la API de Revit requieren que se especifiquen diferentes plataformas.
  • Nombre de ensamblaje – nombre del ensamblado (es decir, nombre del archivo). Puedes escribir el nombre exacto del ensamblado, pero por versatilidad recomiendo escribir el valor $(NombreEnsamblaje)_20хх. Para hacer esto, previamente eliminamos el sufijo del nombre del ensamblaje.

La característica más importante de todos estos elementos es que pueden simplemente copiarse en otros proyectos sin cambiarlos en absoluto. Más adelante en el artículo adjuntaré todo el contenido del archivo .csproj.

Bien, hemos descubierto las propiedades del proyecto; no es difícil. Pero qué hacer con las bibliotecas de complementos (paquetes NuGet). Si miramos más allá, veremos que las bibliotecas incluidas están especificadas en los elementos Grupo de artículos. Pero mala suerte: este elemento procesa incorrectamente las condiciones como elemento. grupo de propiedades. Quizás esto sea incluso un error de Visual Studio, pero si especifica varios elementos Grupo de artículos con las condiciones de configuración e inserte diferentes enlaces a paquetes NuGet en su interior, luego, cuando cambie la configuración, todos los paquetes especificados se conectarán al proyecto.

El elemento viene en nuestra ayuda. Elige, que funciona según nuestra lógica habitual si-entonces-otro.

Usando elemento Elige, configuramos diferentes paquetes NuGet para diferentes configuraciones:

Todos los contenidos csproj

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0"  ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{5AD738D6-4122-4E76-B865-BE7CE0F6B3EB}</ProjectGuid>
    <OutputType>Library</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>MySuperPluginForRevit</RootNamespace>
    <AssemblyName>MySuperPluginForRevit</AssemblyName>
    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <Deterministic>true</Deterministic>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>binDebug</OutputPath>
    <DefineConstants>DEBUG;R2015</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2015|AnyCPU' ">
    <OutputPath>binR2015</OutputPath>
    <DefineConstants>TRACE;R2015</DefineConstants>
    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
    <AssemblyName>$(AssemblyName)_2015</AssemblyName>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2016|AnyCPU' ">
    <OutputPath>binR2016</OutputPath>
    <DefineConstants>TRACE;R2016</DefineConstants>
    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
    <AssemblyName>$(AssemblyName)_2016</AssemblyName>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2017|AnyCPU' ">
    <OutputPath>binR2017</OutputPath>
    <DefineConstants>TRACE;R2017</DefineConstants>
    <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
    <AssemblyName>$(AssemblyName)_2017</AssemblyName>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2018|AnyCPU' ">
    <OutputPath>binR2018</OutputPath>
    <DefineConstants>TRACE;R2018</DefineConstants>
    <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
    <AssemblyName>$(AssemblyName)_2018</AssemblyName>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2019|AnyCPU' ">
    <OutputPath>binR2019</OutputPath>
    <DefineConstants>TRACE;R2019</DefineConstants>
    <TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
    <AssemblyName>$(AssemblyName)_2019</AssemblyName>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2020|AnyCPU' ">
    <OutputPath>binR2020</OutputPath>
    <DefineConstants>TRACE;R2020</DefineConstants>
    <TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
    <AssemblyName>$(AssemblyName)_2020</AssemblyName>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Core" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System.Data" />
    <Reference Include="System.Net.Http" />
    <Reference Include="System.Xml" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Class1.cs" />
    <Compile Include="PropertiesAssemblyInfo.cs" />
  </ItemGroup>
  <Choose>
    <When Condition=" '$(Configuration)'=='R2015' ">
      <ItemGroup>
        <PackageReference Include="ModPlus.Revit.API.2015">
          <Version>1.0.0</Version>
          <ExcludeAssets>runtime</ExcludeAssets>
        </PackageReference>
      </ItemGroup>
    </When>
    <When Condition=" '$(Configuration)'=='R2016' ">
      <ItemGroup>
        <PackageReference Include="ModPlus.Revit.API.2016">
          <Version>1.0.0</Version>
          <ExcludeAssets>runtime</ExcludeAssets>
        </PackageReference>
      </ItemGroup>
    </When>
    <When Condition=" '$(Configuration)'=='R2017' ">
      <ItemGroup>
        <PackageReference Include="ModPlus.Revit.API.2017">
          <Version>1.0.0</Version>
          <ExcludeAssets>runtime</ExcludeAssets>
        </PackageReference>
      </ItemGroup>
    </When>
    <When Condition=" '$(Configuration)'=='R2018' ">
      <ItemGroup>
        <PackageReference Include="ModPlus.Revit.API.2018">
          <Version>1.0.0</Version>
          <ExcludeAssets>runtime</ExcludeAssets>
        </PackageReference>
      </ItemGroup>
    </When>
    <When Condition=" '$(Configuration)'=='R2019' ">
      <ItemGroup>
        <PackageReference Include="ModPlus.Revit.API.2019">
          <Version>1.0.0</Version>
          <ExcludeAssets>runtime</ExcludeAssets>
        </PackageReference>
      </ItemGroup>
    </When>
    <When Condition=" '$(Configuration)'=='R2020' or '$(Configuration)'=='Debug'">
      <ItemGroup>
        <PackageReference Include="ModPlus.Revit.API.2020">
          <Version>1.0.0</Version>
          <ExcludeAssets>runtime</ExcludeAssets>
        </PackageReference>
      </ItemGroup>
    </When>
  </Choose>
  <Import Project="$(MSBuildToolsPath)Microsoft.CSharp.targets" />
</Project>

Tenga en cuenta que en una de las condiciones especifiqué dos configuraciones a través de O. De esta manera el paquete requerido se conectará durante la configuración. Depurar.

Y aquí lo tenemos casi todo perfecto. Volvemos a cargar el proyecto, habilitamos la configuración que necesitamos, llamamos al elemento “ en el menú contextual de la solución (no el proyecto)Restaurar todos los paquetes NuGet"Y vemos cómo cambian nuestros paquetes.

Realizamos un proyecto de complemento con compilación para diferentes versiones de Revit/AutoCAD

Y en esta etapa llegué a un callejón sin salida: para recopilar todas las configuraciones a la vez, podríamos usar el ensamblaje por lotes (menú "asamblea"->"Construcción por lotes"), pero al cambiar de configuración, los paquetes no se restauran automáticamente. Y a la hora de montar el proyecto esto tampoco sucede, aunque, en teoría, debería. No he encontrado una solución a este problema utilizando medios estándar. Y lo más probable es que esto también sea un error de Visual Studio.

Por lo tanto, para el ensamblaje por lotes, se decidió utilizar un sistema de ensamblaje automatizado especial. Nuke. En realidad, no quería esto porque creo que es excesivo en términos de desarrollo de complementos, pero por el momento no veo ninguna otra solución. Y a la pregunta “¿Por qué Nuke?” La respuesta es simple: lo usamos en el trabajo.

Entonces, vaya a la carpeta de nuestra solución (no al proyecto), mantenga presionada la tecla Shift y haga clic derecho en un espacio vacío en la carpeta; en el menú contextual seleccione el elemento "Abra la ventana de PowerShell aquí".

Realizamos un proyecto de complemento con compilación para diferentes versiones de Revit/AutoCAD

Si no lo tienes instalado Nuke, luego primero escribe el comando

dotnet tool install Nuke.GlobalTool –global

Ahora escribe el comando Nuke y se le pedirá que configure Nuke para el proyecto actual. No sé cómo escribir esto más correctamente en ruso; en inglés se escribirá No se pudo encontrar el archivo .nuke. ¿Quieres configurar una compilación? [t/n]

Presione la tecla Y y luego habrá elementos de configuración directa. Necesitamos la opción más simple usando MSBuild, entonces respondemos como en la captura de pantalla:

Realizamos un proyecto de complemento con compilación para diferentes versiones de Revit/AutoCAD

Vayamos a Visual Studio, que nos pedirá que recarguemos la solución, ya que se le ha agregado un nuevo proyecto. Recargamos la solución y vemos que tenemos un proyecto. build en el que estamos interesados ​​en un solo archivo - Construir.cs

Realizamos un proyecto de complemento con compilación para diferentes versiones de Revit/AutoCAD

Abra este archivo y escriba un script para construir el proyecto para todas las configuraciones. Bueno, o usa mi script, que puedes editar según tus necesidades:

using System.IO;
using Nuke.Common;
using Nuke.Common.Execution;
using Nuke.Common.ProjectModel;
using Nuke.Common.Tools.MSBuild;
using static Nuke.Common.Tools.MSBuild.MSBuildTasks;

[CheckBuildProjectConfigurations]
[UnsetVisualStudioEnvironmentVariables]
class Build : NukeBuild
{
    public static int Main () => Execute<Build>(x => x.Compile);

    [Solution] readonly Solution Solution;

    // If the solution name and the project (plugin) name are different, then indicate the project (plugin) name here
    string PluginName => Solution.Name;

    Target Compile => _ => _
        .Executes(() =>
        {
            var project = Solution.GetProject(PluginName);
            if (project == null)
                throw new FileNotFoundException("Not found!");

            var build = new List<string>();
            foreach (var (_, c) in project.Configurations)
            {
                var configuration = c.Split("|")[0];

                if (configuration == "Debug" || build.Contains(configuration))
                    continue;

                Logger.Normal($"Configuration: {configuration}");

                build.Add(configuration);

                MSBuild(_ => _
                    .SetProjectFile(project.Path)
                    .SetConfiguration(configuration)
                    .SetTargets("Restore"));
                MSBuild(_ => _
                    .SetProjectFile(project.Path)
                    .SetConfiguration(configuration)
                    .SetTargets("Rebuild"));
            }
        });
}

Volvemos a la ventana de PowerShell y volvemos a escribir el comando Nuke (puedes escribir el comando Nuke indicando el requerido Target. pero tenemos uno Target, que se ejecuta de forma predeterminada). Después de presionar la tecla Enter, nos sentiremos como verdaderos hackers, porque, como en una película, nuestro proyecto se ensamblará automáticamente para diferentes configuraciones.

Por cierto, puedes usar PowerShell directamente desde Visual Studio (menú "Ver"->"Otras ventanas"->"Consola del administrador de paquetes"), pero todo será en blanco y negro, lo que no es muy conveniente.

Con esto concluye mi artículo. Estoy seguro de que usted mismo podrá encontrar la opción para AutoCAD. Espero que el material aquí presentado encuentre sus “clientes”.

Gracias por su atención!

Fuente: habr.com

Añadir un comentario