Vi laver ét plugin-projekt med kompilering til forskellige versioner af Revit/AutoCAD

Vi laver ét plugin-projekt med kompilering til forskellige versioner af Revit/AutoCAD

Ved udvikling af plugins til CAD-applikationer (i mit tilfælde disse er AutoCAD, Revit og Renga) over tid, dukker et problem op - nye versioner af programmer frigives, deres API-ændringer og nye versioner af plugins skal laves.

Når du kun har ét plugin, eller du stadig er en selvlært begynder i denne sag, kan du blot lave en kopi af projektet, ændre de nødvendige pladser i det og samle en ny version af plugin. Følgelig vil efterfølgende ændringer af kodeksen medføre en multipel stigning i lønomkostningerne.

Efterhånden som du får erfaring og viden, vil du finde flere måder at automatisere denne proces på. Jeg gik denne vej, og jeg vil gerne fortælle dig, hvad jeg endte med, og hvor praktisk det er.

Lad os først se på en metode, der er indlysende, og som jeg har brugt længe.

Links til projektfiler

Og for at gøre alt simpelt, visuelt og forståeligt, vil jeg beskrive alt ved hjælp af et abstrakt eksempel på plugin-udvikling.

Lad os åbne Visual Studio (jeg har Community 2019-versionen. Og ja - på russisk) og skabe en ny løsning. Lad os ringe til ham MySuperPluginForRevit

Vi laver ét plugin-projekt med kompilering til forskellige versioner af Revit/AutoCAD

Vi laver et plugin til Revit til version 2015-2020. Lad os derfor oprette et nyt projekt i løsningen (Net Framework Class Library) og kalde det MySuperPluginForRevit_2015

Vi laver ét plugin-projekt med kompilering til forskellige versioner af Revit/AutoCAD

Vi skal tilføje links til Revit API. Vi kan selvfølgelig tilføje links til lokale filer (vi skal installere alle de nødvendige SDK'er eller alle versioner af Revit), men vi følger straks den rigtige vej og forbinder NuGet-pakken. Du kan finde en del pakker, men jeg vil bruge mine egne.

Efter tilslutning af pakken skal du højreklikke på emnet "RЎSЃS <P "RєRё" og vælg emnet "Flyt packages.config til PackageReference...»

Vi laver ét plugin-projekt med kompilering til forskellige versioner af Revit/AutoCAD

Hvis du pludselig på dette tidspunkt begynder at gå i panik, fordi der i vinduet med pakkens egenskaber ikke er noget vigtigt element "Kopier lokalt", som vi bestemt skal sætte til værdien falsk, så gå ikke i panik - gå til mappen med projektet, åbn filen med filtypen .csproj i en editor, der er praktisk for dig (jeg bruger Notesblok++), og find en post om vores pakke der. Hun ser sådan ud nu:

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

Tilføj en ejendom til den køretid. Det bliver sådan her:

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

Nu, når du bygger et projekt, vil filer fra pakken ikke blive kopieret til output-mappen.
Lad os gå videre – lad os umiddelbart forestille os, at vores plugin vil bruge noget fra Revit API, som har ændret sig over tid, når nye versioner er blevet frigivet. Nå, eller vi skal bare ændre noget i koden afhængigt af den version af Revit, som vi laver pluginnet til. For at løse sådanne forskelle i kode, vil vi bruge betingede kompileringssymboler. Åbn projektegenskaberne, gå til fanen "samling"og i marken"Betinget kompileringsnotation"lad os skrive R2015.

Vi laver ét plugin-projekt med kompilering til forskellige versioner af Revit/AutoCAD

Bemærk, at symbolet skal tilføjes for både Fejlfindings- og Release-konfigurationerne.

Nå, mens vi er i egenskabsvinduet, går vi straks til fanen "App"og i marken"Standard navneområde» fjern suffikset _2015så vores navneområde er universelt og uafhængigt af samlingsnavnet:

Vi laver ét plugin-projekt med kompilering til forskellige versioner af Revit/AutoCAD

I mit tilfælde, i det endelige produkt, er plugins af alle versioner lagt i én mappe, så mine samlingsnavne forbliver med suffikset af formularen _20хх. Men du kan også fjerne suffikset fra samlingsnavnet, hvis filerne skal være placeret i forskellige mapper.

Lad os gå til filkoden Klasse1.cs og simuler noget kode der, under hensyntagen til forskellige versioner af 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;
        }
    }
}

Jeg tog straks højde for alle versioner af Revit ovenfor version 2015 (som var tilgængelige i skrivende stund) og tog straks højde for tilstedeværelsen af ​​betingede kompileringssymboler, som er oprettet ved hjælp af den samme skabelon.

Lad os gå videre til hovedhøjdepunktet. Vi opretter et nyt projekt i vores løsning, kun for versionen af ​​plugin til Revit 2016. Vi gentager alle trinene beskrevet ovenfor, henholdsvis erstatter nummeret 2015 med nummeret 2016. Men filen Klasse1.cs slette fra det nye projekt.

Vi laver ét plugin-projekt med kompilering til forskellige versioner af Revit/AutoCAD

Fil med den nødvendige kode - Klasse1.cs – vi har det allerede, og vi skal bare indsætte et link til det i et nyt projekt. Der er to måder at indsætte links på:

  1. Lang – højreklik på projektet og vælg “Tilføj»->«Eksisterende element", i vinduet, der åbnes, find den ønskede fil og i stedet for indstillingen "Tilføj"vælg muligheden"Tilføj som forbindelse»

Vi laver ét plugin-projekt med kompilering til forskellige versioner af Revit/AutoCAD

  1. kort – direkte i løsningsstifinderen, vælg den ønskede fil (eller endda filer eller endda hele mapper) og træk den ind i et nyt projekt, mens du holder Alt-tasten nede. Mens du trækker, vil du se, at når du trykker på Alt-tasten, vil musemarkøren skifte fra et plustegn til en pil.
    UPS: Jeg lavede lidt forvirring i dette afsnit - for at overføre flere filer skal du holde nede Skift + Alt!

Efter at have udført proceduren, vil vi have en fil i det andet projekt Klasse1.cs med det tilsvarende ikon (blå pil):

Vi laver ét plugin-projekt med kompilering til forskellige versioner af Revit/AutoCAD

Når du redigerer kode i redigeringsvinduet, kan du også vælge, hvilken projektkontekst koden skal vises i, hvilket giver dig mulighed for at se koden redigeres under forskellige betingede kompileringssymboler:

Vi laver ét plugin-projekt med kompilering til forskellige versioner af Revit/AutoCAD

Vi opretter alle andre projekter (2017-2020) ved hjælp af denne ordning. Life hack - hvis du trækker filer i Solution Explorer ikke fra basisprojektet, men fra projektet, hvor de allerede er indsat som et link, så behøver du ikke holde Alt-tasten nede!

Den beskrevne mulighed er ret god, indtil det øjeblik, hvor du tilføjer en ny version af plugin, eller indtil det øjeblik, hvor du tilføjer nye filer til projektet - alt dette bliver meget kedeligt. Og for nylig indså jeg pludselig, hvordan man ordner det hele med et projekt, og vi går videre til den anden metode

Magien ved konfigurationer

Når du har læst her, kan du udbryde: "Hvorfor beskrev du den første metode, hvis artiklen umiddelbart handler om den anden?!" Og jeg beskrev alt for at gøre det tydeligere, hvorfor vi har brug for betingede kompileringssymboler, og på hvilke steder vores projekter adskiller sig. Og nu bliver det tydeligere for os, præcis hvilke forskelle i projekter, vi skal gennemføre, så der kun er ét projekt tilbage.

Og for at gøre alt mere indlysende, vil vi ikke oprette et nyt projekt, men vil foretage ændringer i vores nuværende projekt, der er oprettet på den første måde.

Så først og fremmest fjerner vi alle projekter fra løsningen undtagen det vigtigste (der direkte indeholder filerne). De der. projekter til versioner 2016-2020. Åbn mappen med løsningen og slet mapperne for disse projekter der.

Vi har et projekt tilbage i vores beslutning - MySuperPluginForRevit_2015. Åbn dens egenskaber og:

  1. På fanen "App"fjern suffikset fra samlingsnavnet _2015 (det bliver klart hvorfor senere)
  2. På fanen "samling» fjern det betingede kompileringssymbol R2015 fra det tilsvarende felt

Bemærk: den seneste version af Visual Studio har en fejl - betingede kompileringssymboler vises ikke i vinduet med projektegenskaber, selvom de er tilgængelige. Hvis du oplever denne fejl, skal du fjerne dem manuelt fra .csproj-filen. Vi skal dog stadig arbejde i det, så læs videre.

Omdøb projektet i vinduet Solution Explorer ved at fjerne suffikset _2015 og fjern derefter projektet fra løsningen. Dette er nødvendigt for at opretholde orden og følelser hos perfektionister! Vi åbner mappen med vores løsning, omdøber projektmappen der på samme måde og indlæser projektet tilbage i løsningen.

Åbn konfigurationsmanageren. amerikansk konfiguration Slip i princippet bliver det ikke nødvendigt, så vi sletter det. Vi opretter nye konfigurationer med navne, der allerede er kendte for os R2015, R2016, ..., R2020. Bemærk, at du ikke behøver at kopiere indstillinger fra andre konfigurationer, og du behøver ikke at oprette projektkonfigurationer:

Vi laver ét plugin-projekt med kompilering til forskellige versioner af Revit/AutoCAD

Gå til mappen med projektet, og åbn filen med filtypenavnet .csproj i en editor, der er praktisk for dig. Forresten kan du også åbne det i Visual Studio - du skal fjerne projektet, og så vil det ønskede element være i kontekstmenuen:

Vi laver ét plugin-projekt med kompilering til forskellige versioner af Revit/AutoCAD

Redigering i Visual Studio er endda at foretrække, da editoren både justerer og spørger.

I filen vil vi se elementerne Ejendomsgruppe – helt i top er den generelle, og så kommer forholdene. Disse elementer sætter egenskaberne for projektet, når det bygges. Det første element, som er uden betingelser, sætter generelle egenskaber, og elementer med betingelser ændrer derfor nogle egenskaber afhængigt af konfigurationerne.

Gå til det fælles (første) element Ejendomsgruppe og se på ejendommen Forsamlingsnavn – dette er navnet på forsamlingen, og vi skal have det uden suffiks _2015. Hvis der er et suffiks, så fjern det.

At finde et element med en betingelse

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

Vi har ikke brug for det – vi sletter det.

Element med stand

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

vil være nødvendige for at arbejde på stadiet med kodeudvikling og fejlfinding. Du kan ændre dens egenskaber, så den passer til dine behov - indstille forskellige outputstier, ændre betingede kompileringssymboler osv.

Lad os nu skabe nye elementer Ejendomsgruppe til vores konfigurationer. I disse elementer skal vi blot indstille fire egenskaber:

  • OutputPath – outputmappe. Jeg indstillede standardværdien binR20xx
  • Definer konstanter – betingede kompileringssymboler. Værdien skal angives TRACE;R20хх
  • TargetFrameworkVersion – platform version. Forskellige versioner af Revit API kræver, at forskellige platforme angives.
  • Forsamlingsnavn – samlingsnavn (dvs. filnavn). Du kan skrive det nøjagtige navn på samlingen, men for alsidighed anbefaler jeg at skrive værdien $(AssemblyName)_20хх. For at gøre dette har vi tidligere fjernet suffikset fra samlingsnavnet

Det vigtigste ved alle disse elementer er, at de ganske enkelt kan kopieres til andre projekter uden at ændre dem overhovedet. Senere i artiklen vil jeg vedhæfte alt indholdet af .csproj-filen.

Okay, vi har fundet ud af projektets egenskaber - det er ikke svært. Men hvad skal man gøre med plug-in-biblioteker (NuGet-pakker). Hvis vi kigger videre, vil vi se, at de inkluderede biblioteker er specificeret i elementerne Varegruppe. Men uheld - dette element behandler betingelserne forkert som et element Ejendomsgruppe. Måske er dette endda en Visual Studio-fejl, men hvis du angiver flere elementer Varegruppe med konfigurationsbetingelser, og indsæt forskellige links til NuGet-pakker indeni, så når du ændrer konfigurationen, er alle specificerede pakker forbundet til projektet.

Elementet kommer os til hjælp Vælg, som fungerer efter vores sædvanlige logik hvis-så-ellers.

Brug af element Vælg, indstiller vi forskellige NuGet-pakker til forskellige konfigurationer:

Alt indhold 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>

Bemærk venligst, at jeg i en af ​​betingelserne specificerede to konfigurationer via ELLER. På denne måde vil den nødvendige pakke blive forbundet under konfigurationen Debug.

Og her har vi næsten alt perfekt. Vi indlæser projektet tilbage, aktiverer den konfiguration, vi har brug for, kalder elementet " i kontekstmenuen for løsningen (ikke projektet)Gendan alle NuGet-pakker"og vi ser, hvordan vores pakker ændrer sig.

Vi laver ét plugin-projekt med kompilering til forskellige versioner af Revit/AutoCAD

Og på dette stadium kom jeg til en blindgyde - for at samle alle konfigurationer på én gang kunne vi bruge batchsamling (menu "samling»->«Batch build"), men når der skiftes konfigurationer, gendannes pakker ikke automatisk. Og når man samler projektet, sker dette heller ikke, selvom det i teorien burde. Jeg har ikke fundet en løsning på dette problem ved hjælp af standardmidler. Og højst sandsynligt er dette også en Visual Studio-fejl.

Derfor blev det besluttet at bruge et specielt automatiseret montagesystem til batchmontage Nuke. Jeg ønskede faktisk ikke dette, fordi jeg synes, det er overkill i forhold til udvikling af plugin, men i øjeblikket kan jeg ikke se nogen anden løsning. Og til spørgsmålet "Hvorfor Nuke?" Svaret er enkelt – vi bruger det på arbejdet.

Så gå til mappen med vores løsning (ikke projektet), hold tasten nede Flytte og højreklik på et tomt rum i mappen - i kontekstmenuen vælg punktet "Åbn PowerShell-vinduet her'.

Vi laver ét plugin-projekt med kompilering til forskellige versioner af Revit/AutoCAD

Hvis du ikke har det installeret nuke, så skriv først kommandoen

dotnet tool install Nuke.GlobalTool –global

Skriv nu kommandoen nuke og du vil blive bedt om at konfigurere nuke for det aktuelle projekt. Jeg ved ikke, hvordan jeg skriver dette mere korrekt på russisk - på engelsk vil det blive skrevet. Kunne ikke finde .nuke-filen. Vil du opsætte en build? [y/n]

Tryk på Y-tasten, og så vil der være direkte indstillingspunkter. Vi har brug for den enkleste mulighed ved at bruge MSBuild, så vi svarer som på skærmbilledet:

Vi laver ét plugin-projekt med kompilering til forskellige versioner af Revit/AutoCAD

Lad os gå til Visual Studio, som vil bede os om at genindlæse løsningen, da der er tilføjet et nyt projekt til den. Vi genindlæser løsningen og ser, at vi har et projekt bygge hvor vi kun er interesseret i én fil - Byg.cs

Vi laver ét plugin-projekt med kompilering til forskellige versioner af Revit/AutoCAD

Åbn denne fil og skriv et script til at bygge projektet til alle konfigurationer. Nå, eller brug mit script, som du kan redigere, så det passer til dine behov:

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

Vi vender tilbage til PowerShell-vinduet og skriver kommandoen igen nuke (du kan skrive kommandoen nuke angiver det nødvendige mål. Men vi har en mål, som kører som standard). Efter at have trykket på Enter-tasten vil vi føle os som rigtige hackere, fordi vores projekt, ligesom i en film, automatisk bliver samlet til forskellige konfigurationer.

I øvrigt kan du bruge PowerShell direkte fra Visual Studio (menu "Vis»->«Andre vinduer»->«Pakkehåndteringskonsol"), men alt vil være i sort og hvid, hvilket ikke er særlig praktisk.

Dette afslutter min artikel. Jeg er sikker på, at du selv kan finde ud af muligheden for AutoCAD. Jeg håber, at det her præsenterede materiale vil finde sine "kunder".

Tak for din opmærksomhed!

Kilde: www.habr.com

Tilføj en kommentar