Izdelamo en plugin projekt s kompilacijo za različne različice Revit/AutoCAD

Izdelamo en plugin projekt s kompilacijo za različne različice Revit/AutoCAD

Pri razvoju vtičnikov za aplikacije CAD (v mojem primeru to so AutoCAD, Revit in Renga) se sčasoma pojavi ena težava - izdajajo se nove različice programov, spremenijo se njihovi API-ji in naredijo nove različice vtičnikov.

Ko imate samo en vtičnik ali ste še začetnik samouk, lahko preprosto naredite kopijo projekta, spremenite potrebna mesta v njem in sestavite novo različico vtičnika. Skladno s tem bodo poznejše spremembe kodeksa povzročile večkratno povečanje stroškov dela.

Ko boste pridobili izkušnje in znanje, boste našli več načinov za avtomatizacijo tega procesa. Hodil sem po tej poti in želim vam povedati, kaj sem končal in kako priročno je.

Najprej si poglejmo metodo, ki je očitna in jo uporabljam že dolgo.

Povezave do projektnih datotek

In da bo vse preprosto, vizualno in razumljivo, bom vse opisal z uporabo abstraktnega primera razvoja vtičnikov.

Odprimo Visual Studio (imam različico Community 2019. In ja - v ruščini) in ustvarimo novo rešitev. Pokličimo ga MySuperPluginForRevit

Izdelamo en plugin projekt s kompilacijo za različne različice Revit/AutoCAD

Izdelali bomo vtičnik za Revit za različice 2015-2020. Zato ustvarimo nov projekt v rešitvi (Net Framework Class Library) in ga poimenujmo MySuperPluginForRevit_2015

Izdelamo en plugin projekt s kompilacijo za različne različice Revit/AutoCAD

Dodati moramo povezave do API-ja Revit. Seveda lahko dodamo povezave do lokalnih datotek (namestiti bomo morali vse potrebne SDK-je oz. vse različice Revita), vendar bomo takoj šli po pravi poti in povezali paket NuGet. Najdete lahko kar nekaj paketov, vendar bom uporabil svojega.

Ko povežete paket, z desno miškino tipko kliknite na element "reference" in izberite predmet "Premakni packages.config v PackageReference ...»

Izdelamo en plugin projekt s kompilacijo za različne različice Revit/AutoCAD

Če nenadoma na tej točki začnete paničariti, ker v oknu lastnosti paketa ne bo pomembnega elementa "Kopiraj lokalno", ki ga vsekakor moramo nastaviti na vrednost false, potem brez panike - pojdite v mapo s projektom, odprite datoteko s pripono .csproj v urejevalniku, ki vam ustreza (uporabljam Notepad++) in tam poiščite vnos o našem paketu. Zdaj izgleda takole:

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

Dodajte mu lastnost čas izvajanja. Izkazalo se bo takole:

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

Zdaj, ko gradite projekt, datoteke iz paketa ne bodo kopirane v izhodno mapo.
Pojdimo dlje - takoj si predstavljajmo, da bo naš vtičnik uporabljal nekaj iz API-ja Revit, ki se je s časom spremenil, ko so bile izdane nove različice. No, ali pa moramo le nekaj spremeniti v kodi, odvisno od različice Revita, za katero izdelujemo vtičnik. Za razrešitev takšnih razlik v kodi bomo uporabili simbole pogojnega prevajanja. Odprite lastnosti projekta, pojdite na zavihek »Skupščina"in na terenu"Zapis pogojnega prevajanja"dajmo pisati R2015.

Izdelamo en plugin projekt s kompilacijo za različne različice Revit/AutoCAD

Upoštevajte, da je treba simbol dodati tako za konfiguracijo Debug kot Release.

No, medtem ko smo v oknu lastnosti, takoj gremo na zavihek "Vloga"in na terenu"Privzeti imenski prostor» odstranite pripono _2015tako da je naš imenski prostor univerzalen in neodvisen od imena sklopa:

Izdelamo en plugin projekt s kompilacijo za različne različice Revit/AutoCAD

V mojem primeru so v končnem izdelku vtičniki vseh različic shranjeni v eno mapo, tako da moja imena sklopov ostanejo s pripono obrazca _20хх. Lahko pa tudi odstranite pripono iz imena sklopa, če naj bi se datoteke nahajale v različnih mapah.

Pojdimo na kodo datoteke Class1.cs in tam simuliraj nekaj kode ob upoštevanju različnih različic Revita:

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;
        }
    }
}

Takoj sem upošteval vse različice Revit nad različico 2015 (ki so bile na voljo v času pisanja) in takoj upošteval prisotnost simbolov pogojne kompilacije, ki so ustvarjeni z isto predlogo.

Preidimo na glavni poudarek. V naši rešitvi ustvarimo nov projekt samo za različico vtičnika za Revit 2016. Ponovimo vse zgoraj opisane korake oziroma zamenjamo številko 2015 s številko 2016. Toda datoteka Class1.cs izbrisati iz novega projekta.

Izdelamo en plugin projekt s kompilacijo za različne različice Revit/AutoCAD

Datoteka z zahtevano kodo - Class1.cs – ga že imamo in le povezavo do njega moramo vstaviti v nov projekt. Povezave lahko vstavite na dva načina:

  1. dolga – z desno miškino tipko kliknite projekt in izberite »Dodaj»->«Obstoječi element", v oknu, ki se odpre, poiščite želeno datoteko in namesto možnosti "Dodaj"izberi možnost"Dodaj kot povezavo»

Izdelamo en plugin projekt s kompilacijo za različne različice Revit/AutoCAD

  1. Kratek – neposredno v raziskovalcu rešitev izberite želeno datoteko (ali celo datoteke ali celo celotne mape) in jo med držanjem tipke Alt povlecite v nov projekt. Med vlečenjem boste videli, da se bo kazalec miške, ko pritisnete tipko Alt, spremenil iz znaka plus v puščico.
    UPS: V tem odstavku sem naredil malo zmede - za prenos več datotek morate držati Shift + Alt!

Po izvedbi postopka bomo imeli datoteko v drugem projektu Class1.cs z ustrezno ikono (modra puščica):

Izdelamo en plugin projekt s kompilacijo za različne različice Revit/AutoCAD

Ko urejate kodo v oknu urejevalnika, lahko tudi izberete, v katerem kontekstu projekta želite prikazati kodo, kar vam bo omogočilo, da vidite kodo, ki se ureja pod različnimi simboli pogojnega prevajanja:

Izdelamo en plugin projekt s kompilacijo za različne različice Revit/AutoCAD

Vse ostale projekte (2017-2020) ustvarjamo po tej shemi. Life hack - če datoteke v Raziskovalcu rešitev povlečete ne iz osnovnega projekta, ampak iz projekta, kjer so že vstavljene kot povezava, potem vam ni treba držati tipke Alt!

Opisana možnost je zelo dobra do trenutka dodajanja nove različice vtičnika ali do trenutka dodajanja novih datotek v projekt - vse to postane zelo dolgočasno. In pred kratkim sem nenadoma nenadoma ugotovil, kako vse urediti z enim projektom in prehajamo na drugo metodo

Čarobnost konfiguracij

Ko končate z branjem, boste morda vzkliknili: "Zakaj ste opisali prvo metodo, če je članek takoj o drugi?!" In vse sem opisal, da bo bolj jasno, zakaj potrebujemo simbole pogojne kompilacije in v čem se naši projekti razlikujejo. In zdaj nam postane bolj jasno, kakšne razlike v projektih moramo izvesti, pri čemer ostane samo en projekt.

In da bo vse bolj očitno, ne bomo ustvarili novega projekta, ampak bomo spremenili naš trenutni projekt, ustvarjen na prvi način.

Torej, najprej odstranimo vse projekte iz rešitve, razen glavnega (ki neposredno vsebuje datoteke). Tisti. projekti za različice 2016-2020. Odprite mapo z rešitvijo in tam izbrišite mape teh projektov.

Pri odločitvi imamo še en projekt - MySuperPluginForRevit_2015. Odprite njegove lastnosti in:

  1. Na zavihku "Vloga"odstranite pripono iz imena sklopa _2015 (kasneje bo jasno zakaj)
  2. Na zavihku "Skupščina» odstranite simbol pogojnega prevajanja R2015 iz ustreznega polja

Opomba: najnovejša različica Visual Studio ima napako - simboli pogojne kompilacije niso prikazani v oknu lastnosti projekta, čeprav so na voljo. Če naletite na to napako, jih morate ročno odstraniti iz datoteke .csproj. Vendar moramo v njem še delati, zato berite dalje.

Preimenujte projekt v oknu Raziskovalca rešitev tako, da odstranite pripono _2015 in nato odstranite projekt iz rešitve. To je potrebno za vzdrževanje reda in čustev perfekcionistov! Odpremo mapo naše rešitve, tam preimenujemo projektno mapo na enak način in projekt naložimo nazaj v rešitev.

Odprite upravitelja konfiguracije. Ameriška konfiguracija Sprostite načeloma ne bo rabil, zato ga brišemo. Ustvarimo nove konfiguracije z imeni, ki so nam že znana R2015, R2016, ... R2020. Upoštevajte, da vam ni treba kopirati nastavitev iz drugih konfiguracij in vam ni treba ustvariti konfiguracij projekta:

Izdelamo en plugin projekt s kompilacijo za različne različice Revit/AutoCAD

Pojdite v mapo s projektom in odprite datoteko s pripono .csproj v urejevalniku, ki vam ustreza. Mimogrede, odprete ga lahko tudi v Visual Studio - projekt morate razložiti in nato bo želeni element v kontekstnem meniju:

Izdelamo en plugin projekt s kompilacijo za različne različice Revit/AutoCAD

Urejanje v Visual Studiu je še bolj zaželeno, saj urejevalnik tako poravna kot tudi pozove.

V datoteki bomo videli elemente PropertyGroup – čisto na vrhu je splošna, potem pa pridejo pogoji. Ti elementi določajo lastnosti projekta, ko je zgrajen. Prvi element, ki je brez pogojev, določa splošne lastnosti, elementi s pogoji pa ustrezno spreminjajo nekatere lastnosti glede na konfiguracije.

Pojdite na skupni (prvi) element PropertyGroup in si oglejte nepremičnino Ime sklopa – to je ime sklopa in bi ga morali imeti brez pripone _2015. Če obstaja pripona, jo odstranite.

Iskanje elementa s pogojem

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

Ne potrebujemo ga - izbrišemo ga.

Element s pogojem

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

bodo potrebni za delo na stopnji razvoja kode in odpravljanja napak. Njegove lastnosti lahko spremenite tako, da ustrezajo vašim potrebam - nastavite različne izhodne poti, spremenite simbole pogojnega prevajanja itd.

Sedaj pa ustvarimo nove elemente PropertyGroup za naše konfiguracije. V teh elementih moramo samo nastaviti štiri lastnosti:

  • OutputPath – izhodna mapa. Nastavil sem privzeto vrednost binR20xx
  • DefineConstants – simboli pogojne kompilacije. Vrednost je treba navesti TRACE;R20хх
  • TargetFrameworkVersion – različica platforme. Za različne različice Revit API je treba določiti različne platforme.
  • Ime sklopa – ime sklopa (tj. ime datoteke). Lahko napišete točno ime sklopa, vendar zaradi vsestranskosti priporočam pisanje vrednosti $(AssemblyName)_20хх. Da bi to naredili, smo predhodno odstranili pripono iz imena sklopa

Najpomembnejša značilnost vseh teh elementov je, da jih je mogoče preprosto kopirati v druge projekte, ne da bi jih sploh spremenili. Kasneje v članku bom priložil vso vsebino datoteke .csproj.

V redu, ugotovili smo lastnosti projekta - ni težko. Toda kaj storiti s knjižnicami vtičnikov (paketi NuGet). Če pogledamo naprej, bomo videli, da so vključene knjižnice navedene v elementih ItemGroup. Toda smola - ta element nepravilno obdeluje pogoje kot element PropertyGroup. Morda je to celo napaka Visual Studio, vendar če določite več elementov ItemGroup s konfiguracijskimi pogoji in notri vstavite različne povezave do paketov NuGet, potem ko spremenite konfiguracijo, so vsi navedeni paketi povezani s projektom.

Element nam priskoči na pomoč Izberite, ki deluje po naši običajni logiki če-potem-drugače.

Uporaba elementa Izberite, nastavimo različne pakete NuGet za različne konfiguracije:

Vse vsebine 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>

Upoštevajte, da sem v enem od pogojev določil dve konfiguraciji prek ALI. Tako bo med konfiguracijo povezan zahtevani paket Razhroščevanje.

In tukaj imamo skoraj vse popolno. Projekt naložimo nazaj, omogočimo konfiguracijo, ki jo potrebujemo, pokličemo element " v kontekstnem meniju rešitve (ne projekta)Obnovite vse pakete NuGet"in vidimo, kako se spreminjajo naši paketi.

Izdelamo en plugin projekt s kompilacijo za različne različice Revit/AutoCAD

In na tej stopnji sem prišel v slepo ulico - da bi zbrali vse konfiguracije hkrati, bi lahko uporabili serijsko montažo (meni "Skupščina»->«Paketna izdelava"), toda pri preklapljanju konfiguracij se paketi ne obnovijo samodejno. In pri sestavljanju projekta se to tudi ne zgodi, čeprav bi se teoretično moralo. Nisem našel rešitve te težave s standardnimi sredstvi. In najverjetneje je to tudi napaka Visual Studio.

Zato je bilo za serijsko montažo odločeno uporabiti poseben avtomatiziran sistem montaže Nuke. Tega pravzaprav nisem želel, ker se mi zdi pretirano v smislu razvoja vtičnikov, a trenutno ne vidim druge rešitve. In na vprašanje "Zakaj Nuke?" Odgovor je preprost – uporabljamo ga pri delu.

Torej, pojdite v mapo naše rešitve (ne projekta), držite tipko Shift in z desno miškino tipko kliknite prazen prostor v mapi - v kontekstnem meniju izberite element "Tukaj odprite okno PowerShell".

Izdelamo en plugin projekt s kompilacijo za različne različice Revit/AutoCAD

Če ga nimate nameščenega nuke, potem najprej napišite ukaz

dotnet tool install Nuke.GlobalTool –global

Zdaj napišite ukaz nuke in pozvani boste, da konfigurirate nuke za trenutni projekt. Ne vem, kako to pravilneje napisati v ruščini - v angleščini bo napisano Ni bilo mogoče najti datoteke .nuke. Ali želite nastaviti gradnjo? [da/ne]

Pritisnite tipko Y in nato bodo prikazani elementi neposrednih nastavitev. Potrebujemo najpreprostejšo možnost uporabe MSBuild, zato odgovorimo kot na posnetku zaslona:

Izdelamo en plugin projekt s kompilacijo za različne različice Revit/AutoCAD

Pojdimo v Visual Studio, ki nas bo pozval, da ponovno naložimo rešitev, saj je vanj dodan nov projekt. Ponovno naložimo rešitev in vidimo, da imamo projekt izgradnjo pri katerem nas zanima samo ena datoteka - Build.cs

Izdelamo en plugin projekt s kompilacijo za različne različice Revit/AutoCAD

Odprite to datoteko in napišite skript za izdelavo projekta za vse konfiguracije. No, ali pa uporabite moj skript, ki ga lahko uredite tako, da ustreza vašim potrebam:

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"));
            }
        });
}

Vrnemo se v okno PowerShell in znova napišemo ukaz nuke (lahko napišete ukaz nuke z navedbo zahtevanega ciljna. Ampak enega imamo ciljna, ki se izvaja privzeto). Po pritisku na tipko Enter se bomo počutili kot pravi hekerji, saj bo kot v filmu naš projekt samodejno sestavljen za različne konfiguracije.

Mimogrede, PowerShell lahko uporabite neposredno iz Visual Studio (meni "Poglej»->«Druga okna»->«Konzola upravitelja paketov«), vendar bo vse črno-belo, kar ni zelo priročno.

To zaključuje moj članek. Prepričan sem, da lahko sami ugotovite možnost za AutoCAD. Upam, da bo predstavljeno gradivo našlo svoje "stranke".

Спасибо за внимание!

Vir: www.habr.com

Dodaj komentar