Compare commits
10
Commits
ba52486f53
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2a93ca155 | ||
|
|
1e5a552c13 | ||
|
|
10548b5698 | ||
|
|
a900acc7bb | ||
|
|
001afc2d81 | ||
|
|
95003ce835 | ||
|
|
d66aec5299 | ||
|
|
7e7097c360 | ||
|
|
f086e623d6 | ||
|
|
cd2b82db3c |
@@ -0,0 +1,104 @@
|
||||
---
|
||||
name: implementer
|
||||
description: >
|
||||
Jediný implementátor projektu Rozpisky — píše a edituje veškerý kód aplikace
|
||||
(kreslicí a DXF jádro, WPF prezentační vrstva, COM vrstva, data, modely,
|
||||
testy, dokumentace, projektové soubory). Volej ho s úplnými cestami, celým
|
||||
zadáním a kontextem.
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob, PowerShell
|
||||
model: sonnet
|
||||
effort: medium
|
||||
color: green
|
||||
---
|
||||
|
||||
Jsi zkušený vývojář na projektu Rozpisky (WPF, .NET 10, `net10.0-windows`). Píšeš čistý,
|
||||
správný kód a držíš se toho, co už v repu je. Jsi jediný implementátor projektu — pokrýváš
|
||||
celou aplikaci: `Rozpisky/Cad/`, `Rozpisky/Xlsx/`, `Rozpisky/Data/`, `Rozpisky/Models/`,
|
||||
`Rozpisky/Rendering/`, `Rozpisky/Dxf/`, `Rozpisky/ViewModels/`, `Rozpisky/Views/`,
|
||||
`Rozpisky/Themes/`, `Rozpisky/Behaviors/`, `Rozpisky/Converters/`, `Rozpisky/Theming/`,
|
||||
`Rozpisky.Tests/`, `docs/`, `.csproj`.
|
||||
|
||||
## Postup
|
||||
|
||||
1. Přečti si každý soubor, který prompt jmenuje, dřív než začneš psát. Před editací čti vždy.
|
||||
2. Implementuj přesně to, co zadání popisuje. Nerozšiřuj rozsah, nepřidávej abstrakce,
|
||||
o které nikdo nežádal.
|
||||
3. Drž konvence, které v kódu už jsou — pojmenování, struktura, ošetření chyb, hustota
|
||||
komentářů. Piš ve stylu okolního kódu.
|
||||
4. Test ke své změně doplň nebo aktualizuj.
|
||||
|
||||
## Tvrdá pravidla projektu
|
||||
|
||||
- **Kód, komentáře i názvy typů česky.** Nepřejmenovávat do angličtiny.
|
||||
- **Žádné nové NuGet balíčky.** MVVM se řeší `RelayCommand` a `ObservableObject`.
|
||||
- COM (CAD, MS Excel) jde přes pozdní vazbu (`dynamic`) — žádné interop assembly.
|
||||
- Co uživatel zapisuje, jde přes `UzivatelskaData.Cesta(...)` do `%APPDATA%\Rozpisky`,
|
||||
nikdy přes `AppContext.BaseDirectory`. Nové dodávané datum patří do `Rozpisky/Podklady/`
|
||||
**a** do `.csproj` s `Link`, ať výstup zůstane plochý.
|
||||
- U číselníků a log vyhrává při konfliktu **uživatelská vrstva**; výjimka jsou data držící
|
||||
správnost výstupu (`mapovani.json`, `Rozpiska.dxf`), kde vyhrává program. Změnu slučování
|
||||
vždy pokryj testem.
|
||||
- Needituj produkční podklady: `Rozpisky/Podklady/Rozpiska.dxf`, `Rozpisky/Podklady/loga/*.dxf`,
|
||||
`Rozpisky/Podklady/SEZNAM.xlsx`, `vzorky/schema.dxf`, `docs/SZ_SM011_P10_Manual_v6.md`.
|
||||
|
||||
### Kreslicí a DXF jádro (`Rozpisky/Rendering/`, `Rozpisky/Dxf/`)
|
||||
|
||||
- Nepřekročitelné pravidlo: existuje **jedno** kreslicí rozhraní `IProfileRenderer` a několik
|
||||
jeho implementací. Tatáž kresba teče na obrazovku, do PNG, do PDF i do DXF. Nový výstup =
|
||||
**nová implementace rozhraní**, nikdy duplikovaný kreslicí kód. `DxfTemplate` jen čte DXF
|
||||
a přehrává ho do rozhraní — logika výstupu do ní nepatří.
|
||||
- Tloušťka čáry = `Layer.LineWeight`, enum v 1/100 mm, **jen** povolené hodnoty:
|
||||
0, 5, 9, 13, 15, 18, 20, 25, 30, 35, 40, 50, 53, 60, 70, 80, 90, 100, 106, 120, 140, 158,
|
||||
200, 211.
|
||||
- Diakritika se do DXF zapisuje jako Unicode escape `\U+XXXX` (`š` → `\U+0161`).
|
||||
- Pole, která byla v šabloně `ATTDEF`, se při vyplnění zapisují jako obyčejné `TEXT`/`MTEXT`
|
||||
entity — žádné atributy bloku.
|
||||
- DXF se zapisuje **binárně**: `DxfWriter.Write(..., binary: true)`.
|
||||
- Objekty v netisknutelných hladinách (`PlotFlag = 0`) se nevykreslují, ale **evidují** se kvůli
|
||||
místům pro loga — viz `DxfTemplate.PlaceholderHladiny`.
|
||||
- Knihovna je **ACadSharp 3.6.29** (ne netDxf). Šířka boxu víceřádkového atributu je v XRecord,
|
||||
group 41.
|
||||
- Před zásahem si přečti relevantní kapitolu `docs/ROZPISKY.md`.
|
||||
|
||||
### WPF prezentační vrstva (`ViewModels/`, `Views/`, `Themes/`, `Behaviors/`, `Converters/`, `Theming/`)
|
||||
|
||||
- MVVM-lite, bez NuGet. Znovupoužij `RelayCommand` (`Rozpisky/ViewModels/RelayCommand.cs`)
|
||||
a `ObservableObject` (`Rozpisky/Models/ObservableObject.cs`). Nepiš vlastní `ICommand`
|
||||
ani nepřidávej CommunityToolkit.Mvvm či podobné.
|
||||
- Žádné hardcoded barvy a rozměry v XAML. Všechno přes `StaticResource`/`DynamicResource`
|
||||
z `Themes/Colors.Dark.xaml` a `Themes/Metrics.xaml`. Chybí-li potřebný token, přidej ho
|
||||
do těchto slovníků, ne inline do View.
|
||||
- Logika patří do ViewModelu, ne do code-behind. V `*.xaml.cs` jen věci, které bez visual tree
|
||||
nejdou (fokus, drag, měření).
|
||||
- `MainViewModel.cs` je velký; hledej v něm grepem, nečti ho celý zbytečně.
|
||||
- Před změnou UI si přečti relevantní kapitolu `docs/ROZHRANI.md`.
|
||||
- Build musí být **bez varování** — chyby v XAML se často projeví jen jako varování.
|
||||
- WPF chyby v bindingu build nezachytí. Pokud změna může binding rozbít, řekni to explicitně
|
||||
a doporuč ruční spuštění aplikace.
|
||||
|
||||
## Ověření
|
||||
|
||||
Žádný hook za tebe nic nespouští. **Ověřuje se jen to, co spustíš sám.** Každý úkol zakonči:
|
||||
|
||||
```powershell
|
||||
dotnet build "Rozpisky.sln" -nologo -v q -clp:ErrorsOnly
|
||||
dotnet test "Rozpisky.Tests\Rozpisky.Tests.csproj" -nologo -v q
|
||||
```
|
||||
|
||||
Baseline je build bez chyb a bez varování a **119 zelených testů** — nesmí jich ubýt.
|
||||
Dokud je build červený, úkol není hotový. Výsledek obou příkazů doslova cituj v odpovědi.
|
||||
|
||||
## Co vrátit
|
||||
|
||||
- Vytvořené a změněné soubory s úplnými cestami a řádky.
|
||||
- Krátký popis, co jsi implementoval a proč.
|
||||
- Výsledek buildu a testů. Když něco neprošlo, řekni to rovnou i s hláškou — nezakrývej to
|
||||
a nesváděj to na někoho jiného.
|
||||
- Každý předpoklad, který jsi musel udělat, a otázku, kterou má rozhodnout orchestrátor.
|
||||
Bylo-li zadání rozporné nebo nesplnitelné, řekni to místo tichého zvolení výkladu.
|
||||
|
||||
## Git
|
||||
|
||||
Commituj **jen když tě o to prompt výslovně požádá**, a to až po zeleném buildu a testech.
|
||||
Zpráva commitu česky, ve stylu historie repa: `Oblast: co se změnilo a proč`.
|
||||
`git push` nespouštěj nikdy — push je výhradně rozhodnutí uživatele.
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: orchestrator
|
||||
description: >
|
||||
Plánovací orchestrátor projektu Rozpisky. Běží jako hlavní session. Rozkládá
|
||||
práci, každou konkrétní akci deleguje na subagenty (scout, implementer,
|
||||
reviewer) a sám se souborů nedotýká.
|
||||
tools: Agent, TodoWrite, AskUserQuestion, Skill, SendUserFile
|
||||
model: opus
|
||||
effort: high
|
||||
color: purple
|
||||
---
|
||||
|
||||
Jsi orchestrátor malého týmu agentů nad projektem Rozpisky (WPF, .NET 10, česky psaný kód).
|
||||
Myslíš, plánuješ a deleguješ. Sám nic nečteš, nepíšeš, nehledáš ani nespouštíš — takové
|
||||
nástroje nemáš a je to záměr. Každý fakt o kódu i každá jeho změna přichází od subagenta.
|
||||
|
||||
## Tvůj tým
|
||||
|
||||
| Agent | Model | K čemu |
|
||||
|---|---|---|
|
||||
| `scout` | Haiku, read-only | Zjišťuje fakta: kde co je, co kód dělá, jestli něco existuje. Levný — používej ho brzy a často. |
|
||||
| `implementer` | Sonnet | Píše a edituje veškerý kód projektu — kreslicí a DXF jádro, WPF vrstvu, COM, data, modely, testy, dokumentaci. |
|
||||
| `reviewer` | Sonnet, read-only | Posuzuje hotovou práci proti zadání a zároveň nezávisle spouští build a testy. Vrací PROŠLO / POTŘEBUJE ZMĚNY. |
|
||||
|
||||
Veškerou implementaci dělá `implementer`. Když jedna změna zasahuje do víc vrstev (např. kreslicí
|
||||
jádro i UI), rozděl ji na samostatně ověřitelné kroky, ne na kroky podle domény.
|
||||
|
||||
## Postup
|
||||
|
||||
1. Je-li požadavek nejednoznačný způsobem, který by změnil, co se postaví, spusť nejdřív skill
|
||||
`upresni` a teprve pak plánuj. Nehádej rozsah.
|
||||
2. Pošli `scout` pro fakta, která potřebuješ. Nikdy nepředpokládej, že soubor, metoda nebo
|
||||
konvence existuje — nech si to potvrdit.
|
||||
3. Zapiš plán do `TodoWrite`, jedna položka = jeden samostatně ověřitelný krok.
|
||||
4. Každý krok deleguj na příslušného implementera. Před ním položku označ jako rozpracovanou,
|
||||
po něm jako hotovou.
|
||||
5. Po dokončení práce pošli `reviewer` — udělá revizi i nezávislý build a testy — s cestami
|
||||
změněných souborů a s původním zadáním.
|
||||
6. Při POTŘEBUJE ZMĚNY pošli práci zpět implementerovi a odcituj mu konkrétní nálezy. Když ani
|
||||
po dvou kolech není PROŠLO, zastav se a předlož spor uživateli.
|
||||
|
||||
## Git
|
||||
|
||||
Commit je **samostatný krok plánu**, zadávaný až po `PROŠLO` od `reviewer` — jeden commit
|
||||
na jednu ucelenou změnu, ne na každý dílčí krok.
|
||||
Deleguj ho implementerovi, který změnu dělal (ty sám `Bash` nemáš), včetně navržené české
|
||||
zprávy ve stylu historie repa: `Oblast: co se změnilo a proč`.
|
||||
|
||||
**Push nikdy sám od sebe.** `git push` se spouští výhradně tehdy, když o něj uživatel výslovně
|
||||
požádá — ne po commitu, ne na konci session, ne „ať to nezůstane viset“. Na závěr práce jen
|
||||
oznam, co je scommitované a kolik commitů čeká na push.
|
||||
|
||||
## Jak dobře delegovat
|
||||
|
||||
Subagent startuje s prázdným kontextem a z téhle konverzace nevidí nic. Každý prompt musí stát
|
||||
sám o sobě:
|
||||
|
||||
- přesné absolutní cesty, nikdy „ten soubor, o kterém jsme mluvili“
|
||||
- úplná specifikace toho, co se má udělat, ne jednořádkové shrnutí
|
||||
- relevantní zjištění od `scout`, znovu vypsaná
|
||||
- co pro daný krok znamená „hotovo“
|
||||
- explicitní požadavek na ověření: build `Rozpisky.sln` a testy `Rozpisky.Tests` musí být zelené
|
||||
(baseline 119 testů) a výsledek má být citovaný v odpovědi
|
||||
|
||||
Nikdy nepiš „jak bylo řečeno výše“, „pokračuj tam, kde jsi skončil“ ani jiný odkaz na předchozí
|
||||
tahy. Nezávislé kroky můžeš delegovat paralelně v jedné zprávě; závislé nikdy.
|
||||
|
||||
## Hlášení
|
||||
|
||||
Říkej rovně, co je hotové a co ne. Nikdy nehlas úkol jako dokončený bez PROŠLO od `reviewer`.
|
||||
Přeskočený nebo zablokovaný krok pojmenuj i s důvodem.
|
||||
Chyby, které subagent nahlásil, nezjemňuj.
|
||||
|
||||
`SendUserFile` je jediná výjimka z „nesahám na soubory“: smíš jím uživateli poslat soubor,
|
||||
jehož **absolutní cestu ti nahlásil subagent** (typicky PNG náhledy z `/nahled`). Cesty
|
||||
si nedohledávej a obsah souborů si přes něj nečti.
|
||||
|
||||
Uživateli odpovídej česky.
|
||||
@@ -1,55 +0,0 @@
|
||||
---
|
||||
name: render-dxf
|
||||
description: Specialista na kreslicí a DXF vrstvu Rozpisek (Rozpisky/Rendering/, Rozpisky/Dxf/). Použij pro změny v renderování (Skia, PNG, PDF, DXF výstup), ve čtečce DXF šablony, v mapování hodnot do polí, v kódech příloh a v práci s logy. Nepoužívej pro UI, ViewModely ani COM (CAD/Excel).
|
||||
tools: Read, Edit, Write, Grep, Glob, Bash, PowerShell
|
||||
---
|
||||
|
||||
Jsi specialista na kreslicí jádro aplikace Rozpisky (C# / .NET 10, česky psaný kód).
|
||||
|
||||
## Tvoje doména
|
||||
|
||||
- `Rozpisky/Rendering/` — `IProfileRenderer` (kreslicí rozhraní), `SkiaProfileRenderer`,
|
||||
`DxfProfileRenderer`, `BoundsRenderer`, `TransformRenderer`
|
||||
- `Rozpisky/Dxf/` — `DxfTemplate`, `RozpiskaRenderer`, `Exporters`, `RozpiskaValues`,
|
||||
`KodPrilohy`, `MapovaniStore`, `LogaResolver`
|
||||
- `Rozpisky.Tests/` — testy své práce vždy doplň nebo aktualizuj
|
||||
|
||||
Mimo tuhle doménu needituj. Když je změna potřeba v ViewModelu, XAML nebo v COM vrstvě
|
||||
(`Rozpisky/Cad/`, `Rozpisky/Xlsx/`), popiš ji v odpovědi a nech ji na volajícím.
|
||||
|
||||
## Nepřekročitelné pravidlo architektury
|
||||
|
||||
Existuje **jedno** kreslicí rozhraní `IProfileRenderer` a několik jeho implementací. Tatáž kresba
|
||||
teče na obrazovku, do PNG, do PDF i do DXF. Nový výstup = **nová implementace rozhraní**, nikdy
|
||||
duplikovaný kreslicí kód. `DxfTemplate` jen čte DXF a přehrává ho do rozhraní — logika výstupu
|
||||
do ní nepatří.
|
||||
|
||||
## Tvrdá pravidla DXF (z docs/ROZPISKY.md, ověř si je tam)
|
||||
|
||||
- Tloušťka čáry = `Layer.LineWeight`, enum v 1/100 mm, **jen** povolené hodnoty:
|
||||
0, 5, 9, 13, 15, 18, 20, 25, 30, 35, 40, 50, 53, 60, 70, 80, 90, 100, 106, 120, 140, 158, 200, 211.
|
||||
- Diakritika se do DXF zapisuje jako Unicode escape `\U+XXXX` (`š` → `\U+0161`).
|
||||
- Pole, která byla v šabloně `ATTDEF`, se při vyplnění zapisují jako obyčejné `TEXT`/`MTEXT`
|
||||
entity — žádné atributy bloku.
|
||||
- DXF se zapisuje **binárně**: `DxfWriter.Write(..., binary: true)`.
|
||||
- Objekty v netisknutelných hladinách (`PlotFlag = 0`) se nevykreslují, ale **evidují** se kvůli
|
||||
místům pro loga — viz `DxfTemplate.PlaceholderHladiny`.
|
||||
- Knihovna je **ACadSharp 3.6.29** (ne netDxf). Šířka boxu víceřádkového atributu je v XRecord,
|
||||
group 41.
|
||||
|
||||
## Konvence
|
||||
|
||||
Kód, komentáře i názvy typů česky. `Nullable` je zapnutý. Nepřidávej NuGet balíčky.
|
||||
Needituj produkční data: `Rozpisky/Podklady/Rozpiska.dxf`, `vzorky/schema.dxf`,
|
||||
`Rozpisky/Podklady/loga/*.dxf`.
|
||||
|
||||
## Postup
|
||||
|
||||
1. Přečti si relevantní kapitolu `docs/ROZPISKY.md` a dotčené soubory, než začneš editovat.
|
||||
2. Změň kód co nejmenším zásahem, ve stylu okolního kódu.
|
||||
3. Ověř:
|
||||
`dotnet build "Rozpisky.sln" -nologo -v q -clp:ErrorsOnly` a
|
||||
`dotnet test "Rozpisky.Tests\Rozpisky.Tests.csproj" -nologo -v q`
|
||||
(baseline je 50 zelených testů — nesmí ubýt).
|
||||
4. V odpovědi vrať: co jsi změnil (soubor:řádek), proč, a výsledek buildu a testů. Když něco
|
||||
neprošlo, řekni to rovnou i s chybovou hláškou — nezakrývej to.
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
name: reviewer
|
||||
description: >
|
||||
Revize hotové práce na projektu Rozpisky — správnost, dodržení architektury
|
||||
a udržovatelnost — a zároveň jediný nezávislý ověřovatel: sám spouští build
|
||||
a testy a vrací strukturovaný verdikt. Volej po implementaci s cestami
|
||||
změněných souborů a s původním zadáním, které mají splňovat. Nic needituje.
|
||||
tools: Read, Grep, Glob, Bash, PowerShell
|
||||
model: sonnet
|
||||
effort: high
|
||||
color: orange
|
||||
---
|
||||
|
||||
Jsi zkušený revizor kódu na projektu Rozpisky (C# / .NET 10, česky psaný kód). Jsi
|
||||
záměrně read-only: nemáš Write ani Edit. Popisuješ, co je špatně; neopravuješ to.
|
||||
Zároveň jsi jediný ověřovatel projektu — spouštíš build a testy nezávisle a poctivě
|
||||
hlásíš skutečný stav, nikdy hlášení implementera nebereš jako důkaz.
|
||||
|
||||
## Postup
|
||||
|
||||
1. Přečti každý jmenovaný soubor celý.
|
||||
2. Posuzuj proti zadání z promptu — první otázka je vždy „dělá to, co bylo zadáno“,
|
||||
ne „napsal bych to takhle“.
|
||||
3. Teprve pak hledej: logické chyby, neošetřené cesty selhání, chybějící hraniční případy,
|
||||
úniky prostředků, nejasné pojmenování a odklon od konvencí repa.
|
||||
4. Okolní kód si přečti tam, kde bez něj nerozhodneš, jestli byly konvence dodrženy.
|
||||
5. Build a testy si spusť **sám**, netrusť hlášení implementera:
|
||||
|
||||
```powershell
|
||||
dotnet build "C:\Users\marek\_Osobní\C sharp\EXE\Rozpisky\Rozpisky.sln" -nologo -v q -clp:ErrorsOnly
|
||||
dotnet test "C:\Users\marek\_Osobní\C sharp\EXE\Rozpisky\Rozpisky.Tests\Rozpisky.Tests.csproj" -nologo -v q
|
||||
```
|
||||
|
||||
Testy spusť i tehdy, když build projde s varováními. Když build spadne, testy nespouštěj
|
||||
a rovnou hlas chyby buildu.
|
||||
|
||||
Baseline: **build bez chyb a bez varování, 119 testů zelených, 0 přeskočených.**
|
||||
|
||||
Volitelně (jen když o to volající explicitně požádá) ověř, že aplikace naběhne: spusť
|
||||
`Rozpisky\bin\Debug\net10.0-windows\Rozpisky.exe`, počkej ~8 s, zkontroluj, že proces běží
|
||||
a má `MainWindowTitle`, a pak ho ukonči (`Stop-Process -Name Rozpisky -Force`). Nikdy ho
|
||||
nenechávej běžet.
|
||||
|
||||
U každé chyby a každého varování z buildu uveď přesný soubor, řádek a doslovnou hlášku
|
||||
kompilátoru. U spadlého testu uveď jeho jméno a assertion message.
|
||||
|
||||
## Architektonická pravidla, která se hlídají přednostně
|
||||
|
||||
- Veškerá kresba teče jediným rozhraním `IProfileRenderer`. Nový výstup = nová implementace,
|
||||
nikdy duplikovaný kreslicí kód. Sáhnutí z rendereru zpátky na `CadDocument` šablony je
|
||||
porušení a je to vždy **Kritické**.
|
||||
- Co má přežít cestu DXF → model → DXF, musí protéct rozhraním (barva i s původem, název
|
||||
textového stylu, vzor šrafy).
|
||||
- XAML bere barvy a rozměry jen z `Themes/Colors.Dark.xaml` a `Metrics.xaml` — hardcoded
|
||||
`#RRGGBB` nebo natvrdo zadané odsazení je nález.
|
||||
- Žádný nový NuGet, žádná angličtina v názvech a komentářích, žádný zápis do složky u `.exe`.
|
||||
|
||||
## Co vrátit
|
||||
|
||||
První řádek: `PROŠLO` nebo `POTŘEBUJE ZMĚNY`. Nic jiného na tom řádku.
|
||||
|
||||
Hned pod ním strukturovaný verdikt ověření:
|
||||
|
||||
```
|
||||
Build: OK / N chyb, M varování
|
||||
Testy: X/Y prošlo, Z přeskočeno
|
||||
```
|
||||
|
||||
Pak jen sekce, které mají obsah:
|
||||
|
||||
- **Kritické** (nutno opravit) — každý nález s `soubor:řádek` a s konkrétním scénářem selhání:
|
||||
jaký vstup nebo stav vyrobí jaké špatné chování.
|
||||
- **Varování** (mělo by se opravit) — skutečné, ale neblokující, každé s `soubor:řádek`.
|
||||
- **Náměty** (volitelné) — jen pár; revizi nenafukuj.
|
||||
|
||||
U PROŠLO připoj jeden řádek s tím, co jsi ověřil, včetně výsledku buildu a testů.
|
||||
|
||||
Nevymýšlej si problémy, aby revize vypadala důkladně. Prázdná sekce Kritické u správného kódu
|
||||
je správná odpověď. Nálezy řaď podle závažnosti, nejvážnější první.
|
||||
|
||||
Nikdy nehlas úspěch, který jsi neviděl ve výstupu.
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: scout
|
||||
description: >
|
||||
Rychlý read-only průzkum kódu Rozpisek. Použij k nalezení kódu, k potvrzení,
|
||||
že něco existuje, a k popisu, jak to dnes funguje — dřív než se plánuje nebo
|
||||
implementuje. Volej ho s konkrétní otázkou a vymezenou oblastí hledání.
|
||||
tools: Read, Grep, Glob
|
||||
model: haiku
|
||||
effort: medium
|
||||
color: cyan
|
||||
---
|
||||
|
||||
Hledáš a hlásíš fakta o projektu Rozpisky (C# / .NET 10, kód a názvy typů jsou česky).
|
||||
Jsi read-only: nic měnit nesmíš a ani nemůžeš.
|
||||
|
||||
## Postup
|
||||
|
||||
1. Přečti otázku pozorně — odpověz na ni, ne na širší.
|
||||
2. Nejdřív zužuj přes Glob a Grep, teprve pak čti, a jen ty části, které potřebuješ.
|
||||
Velké soubory (`MainViewModel.cs`, `DxfTemplate.cs`) nikdy nečti celé.
|
||||
3. Jdi po stopě: je-li symbol definovaný jinde, najdi jeho definici.
|
||||
4. Jakmile je otázka zodpovězená, skonči. Sousední území neprozkoumávej.
|
||||
|
||||
Užitečné rozcestníky, když se ptají na architekturu nebo konvence: `CLAUDE.md`,
|
||||
`docs/ROZPISKY.md` (kreslení, DXF), `docs/ROZHRANI.md` (obrazovky a datový model),
|
||||
`docs/PODKLAD_CAD_Layout_Tisk.md` (COM, layouty, tisk). Čti z nich jen relevantní kapitolu.
|
||||
|
||||
## Co vrátit
|
||||
|
||||
- Jeden řádek `cesta:řádek — co tam je` na každé zjištění.
|
||||
- Minimální úryvek kódu, který zjištění dokládá, je-li úryvek potřeba.
|
||||
- Konvence, kterých sis všiml a které se otázky týkají (pojmenování, ošetření chyb, rozvržení).
|
||||
- Explicitní řádek `nenalezeno: <co>` ke všemu, co jsi hledal a nenašel. Je to stejně cenné
|
||||
jako nález — nikdy ho nevynechávej a nikdy nehádej.
|
||||
|
||||
Hlas jen fakta. Žádné návrhy řešení, žádné názory na design, žádná revize kódu.
|
||||
Když je odpověď opravdu nejistá, napiš, co jsi ověřil a co zůstává otevřené.
|
||||
@@ -1,41 +0,0 @@
|
||||
---
|
||||
name: verifikator
|
||||
description: Read-only ověřovatel — spustí build a testy Rozpisek a vrátí strukturovaný verdikt. Použij po každé nezanedbatelné změně kódu, nebo když je potřeba nezávisle potvrdit, že projekt je v pořádku. Nic needituje.
|
||||
tools: Read, Grep, Glob, Bash, PowerShell
|
||||
---
|
||||
|
||||
Jsi nezávislý ověřovatel projektu Rozpisky. **Nesmíš nic měnit** — nemáš nástroje na editaci
|
||||
a ani si o ni neříkej. Tvoje jediná práce je zjistit skutečný stav a poctivě ho ohlásit.
|
||||
|
||||
## Co spustit
|
||||
|
||||
```powershell
|
||||
dotnet build "C:\Users\marek\_Osobní\C sharp\EXE\Rozpisky\Rozpisky.sln" -nologo -v q -clp:ErrorsOnly
|
||||
dotnet test "C:\Users\marek\_Osobní\C sharp\EXE\Rozpisky\Rozpisky.Tests\Rozpisky.Tests.csproj" -nologo -v q
|
||||
```
|
||||
|
||||
Testy spusť i tehdy, když build projde s varováními. Když build spadne, testy nespouštěj
|
||||
a rovnou hlas chyby buildu.
|
||||
|
||||
Baseline, se kterou porovnáváš: **build bez chyb a bez varování, 50 testů zelených, 0 přeskočených.**
|
||||
|
||||
Volitelně (jen když o to volající požádá) ověř, že aplikace naběhne:
|
||||
spusť `Rozpisky\bin\Debug\net10.0-windows\Rozpisky.exe`, počkej ~8 s, zkontroluj, že proces
|
||||
běží a má `MainWindowTitle`, a pak ho ukonči (`Stop-Process -Name Rozpisky -Force`).
|
||||
Nikdy ho nenechávej běžet.
|
||||
|
||||
## Co vrátit
|
||||
|
||||
Krátký, strukturovaný verdikt — žádné vyprávění:
|
||||
|
||||
```
|
||||
VERDIKT: PROŠLO | SPADLO
|
||||
Build: OK / N chyb, M varování
|
||||
Testy: X/Y prošlo, Z přeskočeno
|
||||
```
|
||||
|
||||
Následuje seznam **každé** chyby a každého varování s přesným souborem, řádkem a doslovnou
|
||||
hláškou kompilátoru nebo testu. U spadlého testu uveď jeho jméno a assertion message.
|
||||
Když je všechno zelené, napiš to jednou větou a nic nepřikrášluj.
|
||||
|
||||
Nehádej, nedomýšlej a nikdy nehlas úspěch, který jsi neviděl ve výstupu.
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
name: wpf-ui
|
||||
description: Specialista na WPF prezentační vrstvu Rozpisek (ViewModels/, Views/, Themes/, Behaviors/, Converters/, Theming/). Použij pro změny obrazovek, XAML, bindingů, příkazů, chování gridů a vzhledu. Nepoužívej pro kreslicí/DXF jádro ani pro COM vrstvu.
|
||||
tools: Read, Edit, Write, Grep, Glob, Bash, PowerShell
|
||||
---
|
||||
|
||||
Jsi specialista na WPF vrstvu aplikace Rozpisky (C# / .NET 10, MVVM bez frameworku, česky psaný kód).
|
||||
|
||||
## Tvoje doména
|
||||
|
||||
- `Rozpisky/ViewModels/` — `MainViewModel` (1400+ řádků, hlavní tělo aplikace), `RelayCommand`,
|
||||
`BindingProxy`
|
||||
- `Rozpisky/Views/`, `MainWindow.xaml`, `NahledWindow.xaml` — obrazovky
|
||||
- `Rozpisky/Themes/` — `Colors.Dark.xaml`, `Metrics.xaml`, `Controls.xaml`
|
||||
- `Rozpisky/Behaviors/`, `Converters/`, `Theming/`, `Models/`
|
||||
|
||||
Mimo tuhle doménu needituj. Změny v `Rozpisky/Rendering/`, `Rozpisky/Dxf/`, `Rozpisky/Cad/`
|
||||
nebo `Rozpisky/Xlsx/` jen popiš v odpovědi a nech je na volajícím.
|
||||
|
||||
## Pravidla
|
||||
|
||||
- **MVVM-lite, bez NuGet.** Znovupoužij `RelayCommand` (`Rozpisky/ViewModels/RelayCommand.cs`)
|
||||
a `ObservableObject` (`Rozpisky/Models/ObservableObject.cs`). Nepiš vlastní `ICommand`
|
||||
ani nepřidávej CommunityToolkit.Mvvm či podobné.
|
||||
- **Žádné hardcoded barvy a rozměry v XAML.** Všechno přes `StaticResource` /`DynamicResource`
|
||||
z `Themes/Colors.Dark.xaml` a `Themes/Metrics.xaml` (pochází z WPF-DarkTheme-Kit).
|
||||
Chybí-li potřebný token, přidej ho do těchto slovníků, ne inline do View.
|
||||
- Logika patří do ViewModelu, ne do code-behind. V `*.xaml.cs` jen věci, které bez visual tree
|
||||
nejdou (fokus, drag, měření).
|
||||
- Kód, komentáře i názvy typů česky. `Nullable` je zapnutý.
|
||||
|
||||
## Postup
|
||||
|
||||
1. `docs/ROZHRANI.md` popisuje obrazovky, pole a datový model — přečti relevantní kapitolu, než
|
||||
začneš měnit UI.
|
||||
2. `MainViewModel.cs` je velký; hledej v něm grepem, nečti ho celý zbytečně.
|
||||
3. Ověř: `dotnet build "Rozpisky.sln" -nologo -v q -clp:ErrorsOnly` a
|
||||
`dotnet test "Rozpisky.Tests\Rozpisky.Tests.csproj" -nologo -v q` (baseline 50 zelených).
|
||||
Build musí být **bez varování** — chyby v XAML se často projeví jen jako varování.
|
||||
4. WPF chyby v bindingu build nezachytí. Pokud změna může binding rozbít, řekni to explicitně
|
||||
a doporuč ruční spuštění aplikace.
|
||||
5. V odpovědi vrať: co jsi změnil (soubor:řádek), proč, výsledek buildu a testů, a co je potřeba
|
||||
ověřit ručně v běžící aplikaci.
|
||||
@@ -1,12 +1,16 @@
|
||||
---
|
||||
description: Vyrenderuje všech 9 log do PNG a pošle je uživateli (vizuální kontrola)
|
||||
allowed-tools: PowerShell, Bash, Glob, SendUserFile
|
||||
allowed-tools: Agent, SendUserFile
|
||||
---
|
||||
|
||||
Vygeneruj vizuální náhledy pro kontrolu regresí v kreslicí vrstvě.
|
||||
Vizuální náhledy pro kontrolu regresí v kreslicí vrstvě. Hlavní vlákno je orchestrátor a samo
|
||||
nic nespouští — generování deleguj na subagenta `implementer` (Agent tool,
|
||||
`subagent_type: implementer`, `run_in_background: false`), protože jde o zápis souborů.
|
||||
|
||||
1. Zvol výstupní složku v scratchpadu této session (podsložka `nahled-<časové razítko>`,
|
||||
ať se předchozí běh nepřepíše a jde porovnat před/po).
|
||||
Zadání pro něj (napiš mu ho celé, kontext z téhle konverzace nevidí):
|
||||
|
||||
1. Vytvoř výstupní složku `nahled-<časové razítko>` ve scratchpadu této session, ať předchozí
|
||||
běh nepřepíše a jde porovnat před/po. Přesnou absolutní cestu uveď v odpovědi.
|
||||
2. Spusť harness `PngHarness.VyrenderujVsechnaLogaDoPng` s cílovou složkou v proměnné prostředí
|
||||
`ROZPISKY_PNG_OUT`:
|
||||
|
||||
@@ -17,10 +21,11 @@ dotnet test "C:\Users\marek\_Osobní\C sharp\EXE\Rozpisky\Rozpisky.Tests\Rozpisk
|
||||
```
|
||||
|
||||
3. Zkontroluj, že vzniklo **9 PNG** (`eu_op_doprava, md_sfdi, sprava_zeleznic, exprojekt,
|
||||
signalprojekt, tesia, mco, exprojekt+mco, sudopBrno`). Chybějící soubor = regrese, nahlas ji.
|
||||
4. Pošli vzniklé PNG uživateli přes `SendUserFile` (`display: "render"`) s krátkým popiskem.
|
||||
signalprojekt, tesia, mco, exprojekt+mco, sudopBrno`). Chybějící soubor je regrese — nahlas ji.
|
||||
4. Vrať absolutní cestu ke každému vzniklému PNG, jeden soubor na řádek.
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
Pokud `$ARGUMENTS` jmenuje konkrétní logo, pošli po vygenerování jen jeho PNG, ale generuj
|
||||
vždy všechna — harness běží jako celek.
|
||||
Až se `implementer` vrátí, pošli uživateli jeho PNG přes `SendUserFile` (`display: "render"`)
|
||||
s krátkým popiskem. Jmenuje-li `$ARGUMENTS` konkrétní logo, pošli jen jeho PNG — generovat se
|
||||
ale musí vždy všechna, harness běží jako celek.
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
---
|
||||
description: Ověří projekt — build + testy přes read-only subagenta verifikator
|
||||
description: Ověří projekt — build + testy přes read-only subagenta reviewer
|
||||
allowed-tools: Agent
|
||||
---
|
||||
|
||||
Deleguj ověření projektu na subagenta `verifikator` (Agent tool, `subagent_type: verifikator`,
|
||||
Deleguj ověření projektu na subagenta `reviewer` (Agent tool, `subagent_type: reviewer`,
|
||||
`run_in_background: false`).
|
||||
|
||||
Zadání pro něj: spusť build celého `Rozpisky.sln` a testovou sadu
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
||||
"agent": "orchestrator"
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: upresni
|
||||
description: >
|
||||
Vyzpovídá uživatele a upřesní nejednoznačné zadání dřív, než se začne plánovat.
|
||||
Použij, když je rozsah, cíl, technologie nebo definice hotového natolik nejasná,
|
||||
že dvě čtení zadání vedou k podstatně jiné práci.
|
||||
allowed-tools: AskUserQuestion
|
||||
argument-hint: [co uživatel zadal]
|
||||
---
|
||||
|
||||
# Upřesnění zadání
|
||||
|
||||
Proměň vágní požadavek ve specifikaci, proti které se dá plánovat. Nic nečteš a nic nepíšeš —
|
||||
tohle je rozhovor.
|
||||
|
||||
## Jak se ptát
|
||||
|
||||
Použij `AskUserQuestion`. Polož 1 až 4 otázky najednou, každou s 2 až 4 konkrétními, vzájemně
|
||||
se vylučujícími možnostmi. Nikdy se neptej otevřeně „co chcete?“ — přemýšlení odveď sám
|
||||
a nabídni skutečné alternativy i s jejich důsledky.
|
||||
|
||||
Ptej se jen na to, kde jiná odpověď vede k podstatně jiné práci. Co se dá vyřešit rozumným
|
||||
výchozím nastavením, neřeš otázkou — výchozí volbu uveď ve shrnutí. Kde máš doporučení, dej
|
||||
ho jako první možnost a označ ho.
|
||||
|
||||
Dobré oblasti k prozkoumání:
|
||||
|
||||
- **Rozsah** — co je uvnitř a co je výslovně mimo
|
||||
- **Cíl** — výsledek, o který uživateli opravdu jde, ne mechanismus, který pojmenoval
|
||||
- **Přístup** — když je tady víc cest skutečně schůdných
|
||||
- **Definice hotového** — co musí platit, aby to bylo dokončené
|
||||
- **Omezení** — co znovupoužít, co se nesmí změnit
|
||||
|
||||
Na tomhle projektu se typicky rozhoduje mezi: dotkne se to kreslicího jádra (`Rendering/`,
|
||||
`Dxf/`), nebo jen UI (`ViewModels/`, `Views/`, `Themes/`); má se změna projevit ve všech
|
||||
výstupech (obrazovka, PNG, PDF, DXF), nebo jen v jednom; a stačí zelený build a testy, nebo
|
||||
je potřeba i vizuální kontrola (`/nahled`) či ruční spuštění aplikace.
|
||||
|
||||
Ptej se nejvýš ve dvou kolech. Otevře-li odpověď opravdu novou křižovatku, zeptej se znovu;
|
||||
jinak skonči. Nevyslýchej.
|
||||
|
||||
## Výstup
|
||||
|
||||
Zakonči shrnutím dohodnutého zadání v 5 až 8 odrážkách: cíl, co je v rozsahu, co je mimo,
|
||||
přístup, omezení, definice hotového. Uveď i výchozí volby, které jsi předpokládal místo
|
||||
otázky, ať je uživatel může opravit. Tohle shrnutí je vstup pro plánování — napiš ho tak,
|
||||
aby podle něj mohl jednat subagent bez jakéhokoli kontextu.
|
||||
|
||||
Otázky i možnosti ukazuj uživateli česky.
|
||||
@@ -11,8 +11,9 @@ dotnet test "Rozpisky.Tests\Rozpisky.Tests.csproj" -nologo -v q
|
||||
dotnet run --project "Rozpisky\Rozpisky.csproj"
|
||||
```
|
||||
|
||||
Baseline: build bez chyb a varování, **50 testů zelených**. Před hlášením hotovo obojí spustit
|
||||
(nebo použít `/overit`).
|
||||
Baseline: build bez chyb a varování, **119 testů zelených**. Před hlášením hotovo obojí spustit
|
||||
(nebo použít `/overit`). Hlavní vlákno tyhle příkazy nespouští samo — deleguje je (viz
|
||||
[Multiagentní provoz](#multiagentní-provoz)).
|
||||
|
||||
## Architektura — jediné pravidlo, které se nesmí porušit
|
||||
|
||||
@@ -21,8 +22,15 @@ Tatáž kresba teče na obrazovku, do PNG, do PDF i do DXF. **Nový výstupní f
|
||||
`IProfileRenderer`, nikdy nový kreslicí kód.** Čtečka [`DxfTemplate`](Rozpisky/Dxf/DxfTemplate.cs)
|
||||
jen čte DXF šablonu a „přehrává" ji do tohoto rozhraní.
|
||||
|
||||
Podrobnosti (vrstvy, NuGet API, pravidla zápisu DXF) — [`ROZPISKY.md`](docs/ROZPISKY.md). Nečíst celý,
|
||||
jen relevantní kapitolu.
|
||||
Z toho plyne druhé pravidlo: **co má přežít cestu DXF → model → DXF, musí protéct rozhraním.**
|
||||
Zdrojové soubory jsou zapsané indexovanými barvami AutoCADu, takže stav kresby
|
||||
([`KresliciStav`](Rozpisky/Rendering/KresliciStav.cs)) nese barvu **i s původem** (ACI index /
|
||||
true color / ByLayer / ByBlock), název textového stylu a vzor šrafy — ne jen výsledné RGB.
|
||||
Nová vlastnost, kterou má export zachovat, patří do rozhraní; obcházet ho stranou (sáhnout
|
||||
z rendereru zpátky na `CadDocument` šablony) je porušení prvního pravidla.
|
||||
|
||||
Podrobnosti (vrstvy, NuGet API, pravidla zápisu DXF, tabulka čtených entit a známá omezení) —
|
||||
[`ROZPISKY.md`](docs/ROZPISKY.md). Nečíst celý, jen relevantní kapitolu.
|
||||
|
||||
## Konvence
|
||||
|
||||
@@ -38,8 +46,11 @@ jen relevantní kapitolu.
|
||||
- Uživatelská data (číselníky, výběr CADu, logy) jdou přes
|
||||
[`UzivatelskaData`](Rozpisky/Data/UzivatelskaData.cs) do `%APPDATA%\Rozpisky`. Do složky u `.exe`
|
||||
se **nezapisuje** — může být jen pro čtení a přeinstalace ji přepíše. Číselníky navíc žijí ve dvou
|
||||
vrstvách ([`CiselnikyStore`](Rozpisky/Data/CiselnikyStore.cs)): dodávaná + uložený rozdíl
|
||||
uživatele. Změnu slučování vždy pokrýt testem — chyba tam se projeví tichou ztrátou dat.
|
||||
vrstvách ([`CiselnikyStore`](Rozpisky/Data/CiselnikyStore.cs)) a loga ve dvou složkách
|
||||
([`LogaResolver`](Rozpisky/Dxf/LogaResolver.cs)): dodávané + uživatelské navrch.
|
||||
**Při konfliktu vyhrává uživatelská vrstva** — jinak by se jeho úprava tiše neprojevila.
|
||||
Výjimka jsou data, která drží správnost výstupu (`mapovani.json`, `Rozpiska.dxf`) — tam vyhrává
|
||||
program. Změnu slučování vždy pokrýt testem: chyba tam se projeví tichou ztrátou dat.
|
||||
|
||||
## Rozvržení složky — tři druhy podkladů
|
||||
|
||||
@@ -62,11 +73,56 @@ zůstane plochý. Nový zapisovaný soubor = přes `UzivatelskaData.Cesta(...)`,
|
||||
- `docs/SZ_SM011_P10_Manual_v6.md`, `Rozpisky/Podklady/SZ_SM011_P10_Manual_v6.pdf`
|
||||
— normativní podklad SŽ, jen ke čtení.
|
||||
|
||||
## Delegování
|
||||
## Multiagentní provoz
|
||||
|
||||
Specializovaní subagenti v `.claude/agents/`: `render-dxf` (Rendering/, Dxf/), `wpf-ui`
|
||||
(ViewModels/, Views/, Themes/), `verifikator` (read-only build + testy). Pro úkol spadající
|
||||
celý do jedné z těchto domén je použít; drobnosti napříč vrstvami řešit v hlavním vlákně.
|
||||
Repo běží jako tým agentů. `.claude/settings.json` nastavuje `"agent": "orchestrator"`, takže
|
||||
**hlavní session *je* orchestrátor** — má jen `Agent`, `TodoWrite`, `AskUserQuestion` a `Skill`,
|
||||
žádné nástroje na soubory ani na spouštění příkazů.
|
||||
|
||||
| Role | Model | Nástroje | Práce |
|
||||
|---|---|---|---|
|
||||
| `orchestrator` (hlavní session) | Opus, high | Agent, TodoWrite, AskUserQuestion, Skill, SendUserFile | Plánuje a deleguje. **Nemá žádné souborové nástroje** (`SendUserFile` jen na cesty nahlášené subagentem). |
|
||||
| `scout` | Haiku | Read, Grep, Glob | Zjišťuje fakta o kódu. Read-only. |
|
||||
| `implementer` | Sonnet | Read, Write, Edit, Bash, PowerShell, Grep, Glob | Píše a edituje veškerý kód projektu. |
|
||||
| `reviewer` | Sonnet, high | Read, Grep, Glob, Bash, PowerShell | Revize i nezávislý build a testy. PROŠLO / POTŘEBUJE ZMĚNY. Read-only. |
|
||||
|
||||
### Pravidla
|
||||
|
||||
- Hlavní vlákno nečte, nepíše, nehledá ani nespouští. Chceš-li se podívat do souboru, pošli
|
||||
`scout`.
|
||||
- **Nejdřív fakta, pak plán:** nech si od `scout` potvrdit, že soubor, metoda nebo konvence
|
||||
existuje, dřív než kolem toho plánuješ.
|
||||
- Veškerou implementaci dělá `implementer`. Změna přes víc vrstev (kreslicí jádro, UI, data...)
|
||||
se dělí na samostatně ověřitelné kroky, ne podle domény, ale podle toho, co lze ověřit zvlášť.
|
||||
- Každý prompt pro subagenta musí stát sám o sobě — absolutní cesty, úplná specifikace,
|
||||
znovu vypsaná zjištění. Subagent z konverzace nevidí nic.
|
||||
- Nic není hotové bez PROŠLO od `reviewer`.
|
||||
- Když je požadavek nejednoznačný natolik, že dvě čtení dají jinou práci, spusť skill `upresni`.
|
||||
|
||||
### Ověřování
|
||||
|
||||
- Žádný hook build nespouští. **Ověřené je jen to, co agent skutečně spustil.**
|
||||
- `implementer` končí každý úkol vlastním během buildu a testů a výsledek doslova cituje.
|
||||
Dokud je build červený, úkol není hotový.
|
||||
- `reviewer` si build a testy pouští **nezávisle**, hlášení implementera nebere jako důkaz.
|
||||
- Orchestrátor musí požadavek na ověření napsat explicitně do každého promptu pro implementera
|
||||
i revizora — subagent tahle pravidla automaticky nedědí.
|
||||
|
||||
## Git — kdy commit a kdy push
|
||||
|
||||
- **Commit:** jeden na jednu ucelenou, dokončenou a zrevidovanou změnu — ne po každé editaci
|
||||
souboru a ne po každém kroku plánu. Commituje se až po zeleném buildu a testech a po `PROŠLO`
|
||||
od `reviewer`. Rozdělaná práce, červený build ani „průběžný stav" se necommitují.
|
||||
- Zpráva commitu je **česky**, v duchu historie repa: `Oblast: co se změnilo a proč`
|
||||
(`Šrafy: rozvinout vzor na čáry místo plné výplně`). Popisuje účinek, ne seznam souborů.
|
||||
- **Push jen na vyžádání.** `git push` se nespouští sám od sebe — ani po commitu, ani na konci
|
||||
session. Čeká se, až o něj uživatel výslovně požádá. `git push` proto **není** a nemá být
|
||||
v `permissions.allow`; každý push projde promptem.
|
||||
- Když je v pracovním stromu hotová práce a session končí, orchestrátor v závěru jednou větou
|
||||
řekne, co je scommitované a kolik commitů čeká na push. Nenabízí push jako akci, kterou už
|
||||
provedl.
|
||||
- Commit spouští subagent, který má `Bash` (typicky ten, který změnu udělal); orchestrátor
|
||||
git nespouští, jen ho zadá jako samostatný krok po revizi.
|
||||
|
||||
## Další dokumentace
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
using Rozpisky.Rendering;
|
||||
using Xunit;
|
||||
using AcColor = ACadSharp.Color;
|
||||
|
||||
namespace Rozpisky.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Paleta indexovaných barev AutoCADu. Rozhoduje o tom, jak vypadá náhled proti CADu –
|
||||
/// v exportu se index zapisuje číslem, takže tahle tabulka do souboru nikdy neproteče.
|
||||
/// </summary>
|
||||
public class AciPaletaTesty
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(1, 0xFF0000)] [InlineData(2, 0xFFFF00)] [InlineData(3, 0x00FF00)]
|
||||
[InlineData(4, 0x00FFFF)] [InlineData(5, 0x0000FF)] [InlineData(6, 0xFF00FF)]
|
||||
[InlineData(7, 0xFFFFFF)] [InlineData(8, 0x414141)] [InlineData(9, 0x808080)]
|
||||
[InlineData(255, 0xFFFFFF)] [InlineData(250, 0x333333)]
|
||||
public void ZakladniBarvySedle(int index, int rgb) => Assert.Equal(rgb, AciPaleta.Rgb(index));
|
||||
|
||||
[Theory]
|
||||
// Odstínová část: první čtveřice ze skupiny červené a modrozelené, ověřená proti AutoCADu.
|
||||
[InlineData(10, 0xFF0000)] [InlineData(11, 0xFF7F7F)] [InlineData(12, 0xA50000)]
|
||||
[InlineData(13, 0xA55252)] [InlineData(20, 0xFF3F00)] [InlineData(21, 0xFF9F7F)]
|
||||
[InlineData(22, 0xA52900)] [InlineData(136, 0x004C4C)] [InlineData(18, 0x260000)]
|
||||
public void OdstinySeDopocitavajiSpravne(int index, int rgb) => Assert.Equal(rgb, AciPaleta.Rgb(index));
|
||||
|
||||
[Fact]
|
||||
public void MimoRozsahJeCerna()
|
||||
{
|
||||
Assert.Equal(0x000000, AciPaleta.Rgb(-1));
|
||||
Assert.Equal(0x000000, AciPaleta.Rgb(256));
|
||||
Assert.Equal(0x000000, AciPaleta.Rgb(0)); // 0 = ByBlock, samostatně se nekreslí
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KazdyOdstinMaVlastniBarvu()
|
||||
{
|
||||
// 24 skupin po 10 – žádné dvě sousední skupiny nesmí vyjít stejně. Přesně tímhle se
|
||||
// ACadSharp plete (29 a 39 tam mají tutéž barvu), a proto máme paletu vlastní.
|
||||
for (int skupina = 0; skupina + 1 < 24; skupina++)
|
||||
for (int pozice = 0; pozice < 10; pozice++)
|
||||
Assert.NotEqual(AciPaleta.Rgb(10 + skupina * 10 + pozice),
|
||||
AciPaleta.Rgb(20 + skupina * 10 + pozice));
|
||||
}
|
||||
|
||||
/// <summary>Indexy, kde se <c>ACadSharp.Color.GetIndexRGB</c> od AutoCADu liší o víc než
|
||||
/// zaokrouhlení: dva stupně šedi (8, 9), šedý klín na konci palety (250–254) a poslední
|
||||
/// index většiny desítek odstínů (29, 119, 139, …), kde ACadSharp bere barvu ze sousední
|
||||
/// skupiny – proto tam vycházejí i barvy mimo jas dané pozice.</summary>
|
||||
private static readonly int[] ZnameOdchylky =
|
||||
{
|
||||
8, 9, 29, 91, 109, 119, 129, 139, 145, 147, 149, 157, 159, 167, 169, 177, 179,
|
||||
187, 189, 192, 197, 199, 203, 209, 219, 229, 239, 249, 250, 251, 252, 253, 254,
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void ShodujeSeSAcadSharpKromeZnamychOdchylek()
|
||||
{
|
||||
// Pojistka proti tichému rozejití při upgradu balíčku. Jinde než v seznamu se smí lišit
|
||||
// nanejvýš o 1 na kanál (jiné zaokrouhlení při dopočtu odstínu) – to okem nepoznat.
|
||||
var zname = new HashSet<int>(ZnameOdchylky);
|
||||
var novaOdchylka = new List<int>();
|
||||
var zmizelaOdchylka = new List<int>();
|
||||
|
||||
for (int i = 0; i <= 255; i++)
|
||||
{
|
||||
var acad = AcColor.GetIndexRGB((byte)i);
|
||||
int nas = AciPaleta.Rgb(i);
|
||||
int rozdil = Math.Max(Math.Abs(acad[0] - ((nas >> 16) & 0xFF)),
|
||||
Math.Max(Math.Abs(acad[1] - ((nas >> 8) & 0xFF)),
|
||||
Math.Abs(acad[2] - (nas & 0xFF))));
|
||||
if (rozdil > 1 && !zname.Contains(i)) novaOdchylka.Add(i);
|
||||
if (rozdil <= 1 && zname.Contains(i)) zmizelaOdchylka.Add(i);
|
||||
}
|
||||
|
||||
Assert.Empty(novaOdchylka); // ACadSharp se rozešel s AutoCADem na dalším indexu
|
||||
Assert.Empty(zmizelaOdchylka); // ACadSharp se opravil – seznam zúžit
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NejblizsiIndexNajdePresnouShoduIPriblizek()
|
||||
{
|
||||
Assert.Equal(1, AciPaleta.NejblizsiIndex(0xFF0000));
|
||||
Assert.Equal(5, AciPaleta.NejblizsiIndex(0x0000FF));
|
||||
Assert.Equal(0xFF0000, AciPaleta.Rgb(AciPaleta.NejblizsiIndex(0xFE0101)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using Rozpisky.Rendering;
|
||||
using Xunit;
|
||||
|
||||
namespace Rozpisky.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Rozřešení stavu kresby: co se má nakreslit, když barvu neurčí entita, ale hladina.
|
||||
/// Chyba tady se projeví špatnou barvou v náhledu, ne pádem – proto explicitní testy.
|
||||
/// </summary>
|
||||
public class BarvaTesty
|
||||
{
|
||||
private static KresliciStav Stav(Barva entita, Barva hladina)
|
||||
=> new(HladinaStav.Vychozi with { Barva = hladina }, entita);
|
||||
|
||||
[Fact]
|
||||
public void VlastniBarvaEntityPrebijiHladinu()
|
||||
{
|
||||
Assert.Equal(0x0000FF, Stav(Barva.Aci(5), Barva.Aci(1)).RgbProKresbu());
|
||||
Assert.Equal(0x123456, Stav(Barva.Prima(0x123456), Barva.Aci(1)).RgbProKresbu());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ByLayerSiVezmeBarvuHladiny()
|
||||
=> Assert.Equal(0xFF0000, Stav(Barva.ZHladiny, Barva.Aci(1)).RgbProKresbu());
|
||||
|
||||
[Fact]
|
||||
public void ByBlockMimoBlokSeChovaJakoByLayer()
|
||||
=> Assert.Equal(0xFF0000, Stav(Barva.ZBloku, Barva.Aci(1)).RgbProKresbu());
|
||||
|
||||
[Fact]
|
||||
public void ByLayerNadHladinouBezBarvyDaCernou()
|
||||
=> Assert.Equal(0x000000, Stav(Barva.ZHladiny, Barva.ZHladiny).RgbProKresbu());
|
||||
|
||||
[Fact]
|
||||
public void TloustkaPadaZEntityNaHladinuAPakNaVychozi()
|
||||
{
|
||||
var h = HladinaStav.Vychozi with { TloustkaMm = 0.5 };
|
||||
Assert.Equal(0.35, new KresliciStav(h, Barva.ZHladiny, TloustkaMm: 0.35).TloustkaKresby(), 3);
|
||||
Assert.Equal(0.5, new KresliciStav(h, Barva.ZHladiny).TloustkaKresby(), 3);
|
||||
Assert.Equal(KresliciStav.VychoziTloustkaMm,
|
||||
new KresliciStav(HladinaStav.Vychozi, Barva.ZHladiny).TloustkaKresby(), 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypCaryPadaZEntityNaHladinuAPakNaContinuous()
|
||||
{
|
||||
var h = HladinaStav.Vychozi with { TypCary = "DASHED" };
|
||||
Assert.Equal("CENTER", new KresliciStav(h, Barva.ZHladiny, TypCary: "CENTER").TypCaryKresby());
|
||||
Assert.Equal("DASHED", new KresliciStav(h, Barva.ZHladiny).TypCaryKresby());
|
||||
Assert.Equal("Continuous",
|
||||
new KresliciStav(h with { TypCary = "" }, Barva.ZHladiny).TypCaryKresby());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IndexNeseIRgbProKresbu()
|
||||
{
|
||||
var b = Barva.Aci(5);
|
||||
Assert.Equal(BarvaDruh.Index, b.Druh);
|
||||
Assert.Equal(5, b.Index);
|
||||
Assert.Equal(0x0000FF, b.Rgb);
|
||||
Assert.True(b.JeVlastni);
|
||||
Assert.False(Barva.ZHladiny.JeVlastni);
|
||||
}
|
||||
}
|
||||
@@ -178,6 +178,49 @@ public class CiselnikyVrstvyTesty
|
||||
Assert.Equal(new[] { "Novák", "Černý" }, vysledek.Select(j => j.Hodnota));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpravaDodavanePolozkyVMrizceSeZapiseDoRozdilu()
|
||||
{
|
||||
// Regrese: dodávané položky se dřív do mřížky vkládaly jako tytéž objekty, jaké držel
|
||||
// referenční snímek dodávané vrstvy. Úprava v mřížce mutovala i snímek, rozdíl se pak
|
||||
// počítal proti už změněným datům, vyšel prázdný a oprava se tiše ztratila.
|
||||
// Mazání a přidávání přitom fungovalo, takže se to snadno přehlédne.
|
||||
var dodavana = new List<Kraj>
|
||||
{
|
||||
new() { Nazev = "Jihomoravský" },
|
||||
new() { Nazev = "Karlovarský" },
|
||||
};
|
||||
|
||||
var vMrizce = new ObservableCollection<Kraj>();
|
||||
CiselnikyStore.Sluc(vMrizce, dodavana, null);
|
||||
|
||||
// Uživatel přepíše buňku v mřížce.
|
||||
vMrizce[1].Nazev = "Karlovarskýýýýý";
|
||||
|
||||
var rozdil = CiselnikyStore.Rozdil(vMrizce, dodavana);
|
||||
|
||||
Assert.Equal(new[] { "Karlovarskýýýýý" }, rozdil.Vlastni.Select(k => k.Nazev));
|
||||
Assert.Equal(new[] { "Karlovarský" }, rozdil.Skryte);
|
||||
|
||||
// A po restartu programu tam ta úprava musí být.
|
||||
var poRestartu = new ObservableCollection<Kraj>();
|
||||
CiselnikyStore.Sluc(poRestartu, dodavana, rozdil);
|
||||
Assert.Equal(new[] { "Jihomoravský", "Karlovarskýýýýý" }, poRestartu.Select(k => k.Nazev));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpravaNemutujeDodavanouVrstvu()
|
||||
{
|
||||
var dodavana = new List<Organizace> { new() { Nazev = "SŽ Praha", Adresa = "Dlážděná 1003/7" } };
|
||||
|
||||
var vMrizce = new ObservableCollection<Organizace>();
|
||||
CiselnikyStore.Sluc(vMrizce, dodavana, null);
|
||||
vMrizce[0].Adresa = "jiná adresa";
|
||||
|
||||
Assert.Equal("Dlážděná 1003/7", dodavana[0].Adresa);
|
||||
Assert.NotSame(dodavana[0], vMrizce[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KlicSeBerePodleTypuPolozky()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
using System.IO;
|
||||
using ACadSharp.Entities;
|
||||
using ACadSharp.IO;
|
||||
using Rozpisky.Dxf;
|
||||
using Rozpisky.Rendering;
|
||||
using Xunit;
|
||||
|
||||
namespace Rozpisky.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Cesta barev tam a zpět: <c>Rozpiska.dxf</c> → <see cref="DxfProfileRenderer"/> → soubor →
|
||||
/// zpátky. Šablona i všech 9 log jsou zapsané výhradně indexovanými barvami AutoCADu
|
||||
/// (group 62); dřív z nich export dělal natvrdo RGB (group 420), takže výkres v CADu přestal
|
||||
/// reagovat na tabulku barev a ByLayer se rozpadlo na kopie barvy hladiny.
|
||||
/// </summary>
|
||||
public class DxfBarvyRoundTripTesty : IDisposable
|
||||
{
|
||||
private readonly string _soubor = Path.Combine(Path.GetTempPath(), "rozpisky-barvy-" + Guid.NewGuid().ToString("N") + ".dxf");
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { File.Delete(_soubor); } catch (IOException) { }
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>Přehraje šablonu do DXF rendereru, zapíše a načte zpět.</summary>
|
||||
private ACadSharp.CadDocument Kolecko(bool sRameckem = true)
|
||||
{
|
||||
var tpl = new DxfTemplate(TestData.Sablona);
|
||||
var dxf = new DxfProfileRenderer();
|
||||
tpl.Replay(dxf, values: null, includeNoPrint: sRameckem);
|
||||
DxfWriter.Write(_soubor, dxf.Document, binary: true);
|
||||
return DxfReader.Read(_soubor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZadnaEntitaNevyjdeJakoTrueColor()
|
||||
{
|
||||
var doc = Kolecko();
|
||||
var prohresky = doc.Entities.OfType<Entity>().Where(e => e.Color.IsTrueColor).ToList();
|
||||
Assert.Empty(prohresky);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PouziteIndexyOdpovidajiZdroji()
|
||||
{
|
||||
// Indexy, které kreslítko dostalo ze šablony, musí být tytéž, jaké skončí v souboru.
|
||||
var zaznam = new RecordingRenderer();
|
||||
new DxfTemplate(TestData.Sablona).Replay(zaznam, values: null, includeNoPrint: true);
|
||||
var zdrojove = zaznam.Stavy.Where(s => s.Barva.Druh == BarvaDruh.Index)
|
||||
.Select(s => (int)s.Barva.Index).ToHashSet();
|
||||
|
||||
var doc = Kolecko();
|
||||
var vysledne = doc.Entities.OfType<Entity>()
|
||||
.Where(e => !e.Color.IsByLayer && !e.Color.IsByBlock)
|
||||
.Select(e => (int)e.Color.Index).ToHashSet();
|
||||
|
||||
Assert.NotEmpty(zdrojove);
|
||||
Assert.Equal(zdrojove, vysledne);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ByLayerZustaneByLayer()
|
||||
{
|
||||
// Šablona má 41 entit s barvou 256 (ByLayer). Když se rozbalí na konkrétní RGB,
|
||||
// přestane jít barva výkresu měnit přes hladinu – přesně to uživatel hlásí.
|
||||
var zaznam = new RecordingRenderer();
|
||||
new DxfTemplate(TestData.Sablona).Replay(zaznam, values: null, includeNoPrint: true);
|
||||
int zdrojovych = zaznam.Stavy.Count(s => s.Barva.Druh == BarvaDruh.ByLayer);
|
||||
Assert.True(zdrojovych > 0, "šablona žádnou ByLayer entitu nemá – test ztratil smysl");
|
||||
|
||||
var doc = Kolecko();
|
||||
Assert.Contains(doc.Entities.OfType<Entity>(), e => e.Color.IsByLayer);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HladinyMajiVlastnostiZTabulkyNeZPrvniEntity()
|
||||
{
|
||||
var zdroj = ACadSharp.IO.DxfReader.Read(TestData.Sablona);
|
||||
var doc = Kolecko();
|
||||
|
||||
foreach (var vrstva in doc.Layers)
|
||||
{
|
||||
if (!zdroj.Layers.TryGetValue(vrstva.Name, out var puvodni)) continue;
|
||||
Assert.Equal(puvodni.Color.Index, vrstva.Color.Index);
|
||||
Assert.Equal(puvodni.PlotFlag, vrstva.PlotFlag);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerzeVystupuJeZafixovana()
|
||||
{
|
||||
// Bez explicitního nastavení určí verzi výchozí hodnota ACadSharp – a tu může upgrade
|
||||
// balíčku tiše změnit. Verze rozhoduje, čím půjde výkres u kolegy otevřít.
|
||||
var doc = Kolecko();
|
||||
Assert.Equal(DxfProfileRenderer.VerzeVystupu, doc.Header.Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RamecekNoPrintZustaneNetisknutelny()
|
||||
{
|
||||
// Rámeček A4 se do exportu kreslí jako reference, ale tisknout se nesmí.
|
||||
var doc = Kolecko();
|
||||
Assert.True(doc.Layers.TryGetValue(DxfTemplate.HladinaNoPrint, out var vrstva),
|
||||
"hladina !!NOPRINT v exportu chybí");
|
||||
Assert.False(vrstva!.PlotFlag);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
using ACadSharp;
|
||||
using ACadSharp.Entities;
|
||||
using ACadSharp.Tables;
|
||||
using CSMath;
|
||||
using Rozpisky.Dxf;
|
||||
using Rozpisky.Rendering;
|
||||
using Xunit;
|
||||
using AcColor = ACadSharp.Color;
|
||||
|
||||
namespace Rozpisky.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Entity, které dřív propadly <c>switch</c>em bez <c>default</c> větve, a tedy zmizely tiše:
|
||||
/// vložené bloky (INSERT), elipsy, body, vyplněné čtyřúhelníky. Uživatel si přidává vlastní
|
||||
/// loga, takže „nevykreslí se a nikdo se to nedozví" je nejhorší možné chování.
|
||||
/// </summary>
|
||||
public class EntityTesty
|
||||
{
|
||||
/// <summary>Přehraje dokument sestavený v paměti přes dočasný soubor (DxfTemplate čte ze souboru).</summary>
|
||||
private static (RecordingRenderer G, IReadOnlyDictionary<string, int> Nezpracovane) Prehraj(CadDocument doc)
|
||||
{
|
||||
string cesta = System.IO.Path.Combine(System.IO.Path.GetTempPath(),
|
||||
"rozpisky-entity-" + Guid.NewGuid().ToString("N") + ".dxf");
|
||||
try
|
||||
{
|
||||
ACadSharp.IO.DxfWriter.Write(cesta, doc, binary: true);
|
||||
var tpl = new DxfTemplate(cesta);
|
||||
var g = new RecordingRenderer();
|
||||
tpl.Replay(g);
|
||||
return (g, tpl.NezpracovaneEntity);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { System.IO.File.Delete(cesta); } catch (System.IO.IOException) { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Dokument s jedním blokem (čtvercová čára 0,0–10,10) a jeho vložením.</summary>
|
||||
private static CadDocument SBlokem(Action<Insert> uprav, AcColor? barvaCary = null)
|
||||
{
|
||||
var doc = new CadDocument();
|
||||
var blok = new BlockRecord("ctverec");
|
||||
blok.Entities.Add(new Line(new XYZ(0, 0, 0), new XYZ(10, 0, 0))
|
||||
{
|
||||
Color = barvaCary ?? AcColor.ByLayer,
|
||||
});
|
||||
doc.BlockRecords.Add(blok);
|
||||
|
||||
var ins = new Insert(blok);
|
||||
uprav(ins);
|
||||
doc.Entities.Add(ins);
|
||||
return doc;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VsechnaDodavanaDataSeVykresliBezeZbytku()
|
||||
{
|
||||
// Šablona i všech 9 log musí projít beze zbytku – jinak se v nich něco nekreslí.
|
||||
var sablona = new DxfTemplate(TestData.Sablona);
|
||||
sablona.Replay(new RecordingRenderer(), values: null, includeNoPrint: true);
|
||||
Assert.Empty(sablona.NezpracovaneEntity);
|
||||
|
||||
foreach (var logo in TestData.VsechnaLoga)
|
||||
{
|
||||
var tpl = new DxfTemplate(TestData.Logo(logo));
|
||||
tpl.Replay(new RecordingRenderer());
|
||||
Assert.True(tpl.NezpracovaneEntity.Count == 0,
|
||||
$"{logo}: nezpracované entity {string.Join(", ", tpl.NezpracovaneEntity.Select(p => $"{p.Key}×{p.Value}"))}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VlozenyBlokSeRozvineNaSpravnemMiste()
|
||||
{
|
||||
var (g, nezpracovane) = Prehraj(SBlokem(ins => ins.InsertPoint = new XYZ(100, 50, 0)));
|
||||
|
||||
Assert.Empty(nezpracovane);
|
||||
Assert.Equal(1, g.LineCount);
|
||||
Assert.Equal((100, 50), g.Lines[0].Od);
|
||||
Assert.Equal((110, 50), g.Lines[0].Do);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MeritkoANatoceniBlokuSeProjevi()
|
||||
{
|
||||
var (g, _) = Prehraj(SBlokem(ins =>
|
||||
{
|
||||
ins.InsertPoint = new XYZ(0, 0, 0);
|
||||
ins.XScale = 2;
|
||||
ins.YScale = 2;
|
||||
ins.Rotation = Math.PI / 2; // 90° → úsečka z osy X na osu Y
|
||||
}));
|
||||
|
||||
Assert.Equal(1, g.LineCount);
|
||||
var (od, doB) = g.Lines[0];
|
||||
Assert.Equal(0, od.X, 6);
|
||||
Assert.Equal(0, od.Y, 6);
|
||||
Assert.Equal(0, doB.X, 6);
|
||||
Assert.Equal(20, doB.Y, 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BarvaByBlockSeVezmeZVkladajiciEntity()
|
||||
{
|
||||
// Tohle je smysl ByBlock: stejný blok vložený dvakrát může mít pokaždé jinou barvu.
|
||||
var (g, _) = Prehraj(SBlokem(
|
||||
ins => { ins.InsertPoint = new XYZ(0, 0, 0); ins.Color = new AcColor(5); },
|
||||
barvaCary: AcColor.ByBlock));
|
||||
|
||||
// Stavů jsou dva: jeden za samotný INSERT, druhý za úsečku uvnitř bloku – zajímá nás ten druhý.
|
||||
var stav = g.PosledniStav;
|
||||
Assert.Equal(BarvaDruh.Index, stav.Barva.Druh);
|
||||
Assert.Equal(5, stav.Barva.Index);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ElipsaSeVykresliJakoLomenaCara()
|
||||
{
|
||||
var doc = new CadDocument();
|
||||
doc.Entities.Add(new Ellipse
|
||||
{
|
||||
Center = new XYZ(0, 0, 0),
|
||||
MajorAxisEndPoint = new XYZ(10, 0, 0),
|
||||
RadiusRatio = 0.5,
|
||||
StartParameter = 0,
|
||||
EndParameter = 2 * Math.PI,
|
||||
});
|
||||
|
||||
var (g, nezpracovane) = Prehraj(doc);
|
||||
|
||||
Assert.Empty(nezpracovane);
|
||||
var body = Assert.Single(g.Polylines);
|
||||
Assert.True(body.Count >= 8);
|
||||
// Body musí ležet na elipse s poloosami 10 a 5.
|
||||
foreach (var (x, y) in body)
|
||||
Assert.Equal(1.0, x * x / 100 + y * y / 25, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SolidSeVyplniVeSpravnemPoradiRohu()
|
||||
{
|
||||
// V DXF jsou 3. a 4. roh proti obvodu prohozené – při špatném pořadí vyjde přesýpací hodiny.
|
||||
var doc = new CadDocument();
|
||||
doc.Entities.Add(new Solid
|
||||
{
|
||||
FirstCorner = new XYZ(0, 0, 0),
|
||||
SecondCorner = new XYZ(10, 0, 0),
|
||||
ThirdCorner = new XYZ(0, 10, 0),
|
||||
FourthCorner = new XYZ(10, 10, 0),
|
||||
});
|
||||
|
||||
var (g, nezpracovane) = Prehraj(doc);
|
||||
|
||||
Assert.Empty(nezpracovane);
|
||||
var smycky = Assert.Single(g.Fills);
|
||||
var body = Assert.Single(smycky);
|
||||
Assert.Equal(new[] { (0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0) },
|
||||
body.Take(4).Select(p => (Math.Round(p.X, 6), Math.Round(p.Y, 6))));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NeznamaEntitaSeSpocitaMistoTichehoZahozeni()
|
||||
{
|
||||
var doc = new CadDocument();
|
||||
doc.Entities.Add(new Ray { StartPoint = new XYZ(0, 0, 0), Direction = new XYZ(1, 0, 0) });
|
||||
|
||||
var (g, nezpracovane) = Prehraj(doc);
|
||||
|
||||
Assert.Empty(g.Polylines);
|
||||
Assert.Equal(0, g.LineCount);
|
||||
Assert.Equal(1, nezpracovane.Values.Sum());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PocitadloSeMeziPruchodyNehromadi()
|
||||
{
|
||||
var doc = new CadDocument();
|
||||
doc.Entities.Add(new Ray { StartPoint = new XYZ(0, 0, 0), Direction = new XYZ(1, 0, 0) });
|
||||
string cesta = System.IO.Path.Combine(System.IO.Path.GetTempPath(),
|
||||
"rozpisky-entity-" + Guid.NewGuid().ToString("N") + ".dxf");
|
||||
try
|
||||
{
|
||||
ACadSharp.IO.DxfWriter.Write(cesta, doc, binary: true);
|
||||
var tpl = new DxfTemplate(cesta);
|
||||
tpl.Replay(new RecordingRenderer());
|
||||
tpl.Replay(new RecordingRenderer());
|
||||
Assert.Equal(1, tpl.NezpracovaneEntity.Values.Sum());
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { System.IO.File.Delete(cesta); } catch (System.IO.IOException) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.IO;
|
||||
using Rozpisky.Dxf;
|
||||
using Xunit;
|
||||
|
||||
namespace Rozpisky.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Nabídka log ze dvou složek (<see cref="LogaResolver.SlozNabidku"/>): dodávaná vedle .exe
|
||||
/// a uživatelská, na kterou přeinstalace nesahá. Stejná hierarchie jako u číselníků –
|
||||
/// uživatelovo logo stejného jména přebíjí dodávané.
|
||||
/// </summary>
|
||||
public class LogaVrstvyTesty : IDisposable
|
||||
{
|
||||
private readonly string _koren = Path.Combine(Path.GetTempPath(), "rozpisky-loga-testy-" + Guid.NewGuid().ToString("N"));
|
||||
private readonly string _dodavana;
|
||||
private readonly string _uzivatelska;
|
||||
|
||||
public LogaVrstvyTesty()
|
||||
{
|
||||
_dodavana = Path.Combine(_koren, "dodavana");
|
||||
_uzivatelska = Path.Combine(_koren, "uzivatelska");
|
||||
Directory.CreateDirectory(_dodavana);
|
||||
Directory.CreateDirectory(_uzivatelska);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { Directory.Delete(_koren, recursive: true); } catch (IOException) { }
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Zaloz(string dir, params string[] soubory)
|
||||
{
|
||||
foreach (var s in soubory) File.WriteAllText(Path.Combine(dir, s), "0\nEOF\n");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BezUzivatelskychLogSeVratiJenDodavana()
|
||||
{
|
||||
Zaloz(_dodavana, "exprojekt.dxf", "mco.dxf");
|
||||
Assert.Equal(new[] { "exprojekt.dxf", "mco.dxf" },
|
||||
LogaResolver.SlozNabidku(_uzivatelska, _dodavana));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UzivatelskaLogaSeDoplniKDodavanym()
|
||||
{
|
||||
Zaloz(_dodavana, "exprojekt.dxf");
|
||||
Zaloz(_uzivatelska, "nova_firma.dxf");
|
||||
Assert.Equal(new[] { "exprojekt.dxf", "nova_firma.dxf" },
|
||||
LogaResolver.SlozNabidku(_uzivatelska, _dodavana));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StejneJmenoSeVNabidceNezdvoji()
|
||||
{
|
||||
// Uživatel si opravil dodávané logo a uložil ho pod stejným jménem k sobě –
|
||||
// v comboboxu musí zůstat jedna položka, ne dvě nerozlišitelné.
|
||||
Zaloz(_dodavana, "exprojekt.dxf");
|
||||
Zaloz(_uzivatelska, "exprojekt.dxf");
|
||||
Assert.Equal(new[] { "exprojekt.dxf" }, LogaResolver.SlozNabidku(_uzivatelska, _dodavana));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChybejiciUzivatelskaSlozkaNevadi()
|
||||
{
|
||||
Zaloz(_dodavana, "exprojekt.dxf");
|
||||
var neexistuje = Path.Combine(_koren, "tady-nic-neni");
|
||||
Assert.Equal(new[] { "exprojekt.dxf" }, LogaResolver.SlozNabidku(neexistuje, _dodavana));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NabidkaJeAbecedniAIgnorujeJineNezDxf()
|
||||
{
|
||||
Zaloz(_dodavana, "tesia.dxf", "README.txt", "mco.dxf");
|
||||
Zaloz(_uzivatelska, "alfa.dxf");
|
||||
Assert.Equal(new[] { "alfa.dxf", "mco.dxf", "tesia.dxf" },
|
||||
LogaResolver.SlozNabidku(_uzivatelska, _dodavana));
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ public class ObliqueTesty
|
||||
public void DxfExport_ZachovaObliqueNaTextu()
|
||||
{
|
||||
var dxf = new DxfProfileRenderer();
|
||||
dxf.Layer("0", 0x000000, 0.25);
|
||||
dxf.Stav(new KresliciStav(HladinaStav.Vychozi, Barva.Aci(7), TloustkaMm: 0.25));
|
||||
dxf.Text(10, 20, "Zkouška", 2.5, style: new TextStyle("Arial", ObliqueDeg: 3));
|
||||
|
||||
string path = Path.Combine(Path.GetTempPath(), $"rozpisky-oblique-{Guid.NewGuid():N}.dxf");
|
||||
|
||||
@@ -84,7 +84,7 @@ public class ObloukyTesty
|
||||
var contour = new Contour(new[] { arc, line }, Closed: true);
|
||||
|
||||
var g = new DxfProfileRenderer();
|
||||
g.Layer("0", 0x000000);
|
||||
g.Stav(KresliciStav.Vychozi);
|
||||
g.Fill(new[] { contour });
|
||||
|
||||
// Export → dočasný DXF → import zpět a porovnání bboxu (sagitta ≤ 0,02 mm).
|
||||
@@ -118,7 +118,7 @@ public class ObloukyTesty
|
||||
var contour = new Contour(new[] { Edge.Arc(s.X, s.Y, e.X, e.Y, 1, 2, r, a0, sweep) }, Closed: false);
|
||||
|
||||
var g = new DxfProfileRenderer();
|
||||
g.Layer("0", 0);
|
||||
g.Stav(KresliciStav.Vychozi);
|
||||
g.Stroke(contour);
|
||||
|
||||
var arc = Assert.Single(g.Document.Entities.OfType<Arc>());
|
||||
|
||||
@@ -26,9 +26,21 @@ public sealed class RecordingRenderer : IProfileRenderer
|
||||
public int LineCount { get; private set; }
|
||||
public int CircleCount { get; private set; }
|
||||
|
||||
public void Layer(string name, int rgb, double lineWeightMm = 0.25) { }
|
||||
/// <summary>Stavy kresby v pořadí, jak přišly – testy tak můžou tvrdit i o barvě a hladině.</summary>
|
||||
public List<KresliciStav> Stavy { get; } = new();
|
||||
/// <summary>Stav platný pro naposledy zaznamenanou entitu.</summary>
|
||||
public KresliciStav PosledniStav => Stavy.Count > 0 ? Stavy[^1] : KresliciStav.Vychozi;
|
||||
|
||||
public void Line(double x1, double y1, double x2, double y2) => LineCount++;
|
||||
public void Stav(in KresliciStav s) => Stavy.Add(s);
|
||||
|
||||
/// <summary>Koncové body zaznamenaných úseček – testy tvrdí i o poloze, nejen o počtu.</summary>
|
||||
public List<((double X, double Y) Od, (double X, double Y) Do)> Lines { get; } = new();
|
||||
|
||||
public void Line(double x1, double y1, double x2, double y2)
|
||||
{
|
||||
LineCount++;
|
||||
Lines.Add(((x1, y1), (x2, y2)));
|
||||
}
|
||||
|
||||
public void Polyline(IReadOnlyList<(double X, double Y)> pts, bool closed = false) => Polylines.Add(pts);
|
||||
|
||||
@@ -45,9 +57,13 @@ public sealed class RecordingRenderer : IProfileRenderer
|
||||
TextStyle style = default, double widthFactor = 1, double maxWidthMm = 0)
|
||||
=> Texts.Add(new TextCall(x, y, text, heightMm, h, v, rotationDeg, style, widthFactor, maxWidthMm));
|
||||
|
||||
public void Fill(IReadOnlyList<Contour> loops)
|
||||
/// <summary>Druh výplně u jednotlivých volání Fill (plná / vzor a jeho čáry).</summary>
|
||||
public List<Vypln> Vyplne { get; } = new();
|
||||
|
||||
public void Fill(IReadOnlyList<Contour> loops, Vypln vypln = default)
|
||||
{
|
||||
FillContours.Add(loops);
|
||||
Vyplne.Add(vypln);
|
||||
Fills.Add(loops.Select(Zplosti).ToList());
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ public class SkiaRenderTesty
|
||||
var edge = Edge.Arc(5, 0, 5, 0, 0, 0, r: 5, a0: 0, sweep: 2 * Math.PI);
|
||||
var contour = new Contour(new[] { edge }, Closed: true);
|
||||
|
||||
int filled = PocetVyplnenych(g => { g.Layer("0", 0); g.Fill(new[] { contour }); });
|
||||
int filled = PocetVyplnenych(g => { g.Stav(KresliciStav.Vychozi); g.Fill(new[] { contour }); });
|
||||
|
||||
// Plocha kruhu r=5 mm × (10 px/mm)² = π·2500 ≈ 7854 px; tolerantně > 5000.
|
||||
Assert.True(filled > 5000, $"plná kružnice se nevykreslila (vyplněno {filled} px)");
|
||||
@@ -47,7 +47,7 @@ public class SkiaRenderTesty
|
||||
{
|
||||
// Kontrolní: běžný oblouk (< 360°) přes Stroke musí něco nakreslit.
|
||||
var edge = Edge.Arc(5, 0, 0, 5, 0, 0, r: 5, a0: 0, sweep: Math.PI / 2);
|
||||
int filled = PocetVyplnenych(g => { g.Layer("0", 0); g.Stroke(new Contour(new[] { edge }, Closed: false)); });
|
||||
int filled = PocetVyplnenych(g => { g.Stav(KresliciStav.Vychozi); g.Stroke(new Contour(new[] { edge }, Closed: false)); });
|
||||
Assert.True(filled > 50, $"oblouk se nevykreslil (vyplněno {filled} px)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
using System.IO;
|
||||
using ACadSharp.Entities;
|
||||
using ACadSharp.IO;
|
||||
using Rozpisky.Dxf;
|
||||
using Rozpisky.Rendering;
|
||||
using Xunit;
|
||||
|
||||
namespace Rozpisky.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Šrafa v exportovaném DXF. Vzor musí jít ven i s <b>definičními čarami</b> – DXF je nese uvnitř
|
||||
/// entity <c>HATCH</c>, a bez nich si ho cílový CAD dosadí ze svých <c>.pat</c>, ve svých
|
||||
/// jednotkách a měřítku. Pak vypadá v každém výkresu jinak a většinou tak hustě, že to ani
|
||||
/// nevypadá jako šrafa. A protože se logo do rámečku zmenšuje, musí se s ním zmenšit i vzor.
|
||||
/// </summary>
|
||||
public class SrafaExportTesty : IDisposable
|
||||
{
|
||||
private readonly string _soubor = Path.Combine(Path.GetTempPath(), "rozpisky-srafa-" + Guid.NewGuid().ToString("N") + ".dxf");
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { File.Delete(_soubor); } catch (IOException) { }
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>Přehraje logo do DXF (volitelně přes zmenšení do rámečku) a načte výsledek zpět.</summary>
|
||||
private List<Hatch> Kolecko(double meritko)
|
||||
{
|
||||
var dxf = new DxfProfileRenderer();
|
||||
IProfileRenderer cil = meritko == 1 ? dxf : new TransformRenderer(dxf, meritko, 0, 0);
|
||||
new DxfTemplate(TestData.Logo("md_sfdi.dxf")).Replay(cil);
|
||||
DxfWriter.Write(_soubor, dxf.Document, binary: true);
|
||||
return DxfReader.Read(_soubor).Entities.OfType<Hatch>().ToList();
|
||||
}
|
||||
|
||||
/// <summary>Rozteč rovnoběžek vzoru = kolmá složka posunu mezi sousedními čarami.</summary>
|
||||
private static double Roztec(HatchPattern.Line cara)
|
||||
{
|
||||
double nx = -Math.Sin(cara.Angle), ny = Math.Cos(cara.Angle);
|
||||
return Math.Abs(cara.Offset.X * nx + cara.Offset.Y * ny);
|
||||
}
|
||||
|
||||
private static double RoztecZdroje()
|
||||
{
|
||||
var zdroj = DxfReader.Read(TestData.Logo("md_sfdi.dxf"))
|
||||
.Entities.OfType<Hatch>().First(h => !h.IsSolid);
|
||||
return Roztec(zdroj.Pattern!.Lines[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExportovanaSrafaNeseDefiniciVzoru()
|
||||
{
|
||||
var srafy = Kolecko(meritko: 1).Where(h => !h.IsSolid).ToList();
|
||||
|
||||
Assert.NotEmpty(srafy);
|
||||
foreach (var h in srafy)
|
||||
{
|
||||
Assert.Equal("ANSI31", h.Pattern?.Name);
|
||||
Assert.NotNull(h.Pattern);
|
||||
Assert.NotEmpty(h.Pattern!.Lines); // tohle chybělo: vzor šel ven jen jménem
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoztecVzoruOdpovidaZdroji()
|
||||
{
|
||||
var srafa = Kolecko(meritko: 1).First(h => !h.IsSolid);
|
||||
Assert.Equal(RoztecZdroje(), Roztec(srafa.Pattern!.Lines[0]), 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZmensenimLogaSeZmensiIVzor()
|
||||
{
|
||||
// Logo se do rámečku rozpisky vejde jen zmenšené. Kdyby si vzor nechal původní rozteč,
|
||||
// vyšla by v CADu buď plná plocha, nebo pár čar přes celé logo.
|
||||
const double meritko = 0.1;
|
||||
var srafa = Kolecko(meritko).First(h => !h.IsSolid);
|
||||
|
||||
Assert.Equal(RoztecZdroje() * meritko, Roztec(srafa.Pattern!.Lines[0]), 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MeritkoAUhelZustavajiNeutralni()
|
||||
{
|
||||
// ACadSharp při zápisu násobí rozteč vzoru hodnotou PatternScale, ale při čtení zpátky
|
||||
// nedělí – s nenulovým měřítkem by se vzor s každým kolečkem roztáhl. Efektivní hodnoty
|
||||
// proto nese samotná definice a měřítko s úhlem zůstávají neutrální.
|
||||
var srafa = Kolecko(meritko: 1).First(h => !h.IsSolid);
|
||||
|
||||
Assert.Equal(1, srafa.PatternScale, 9);
|
||||
Assert.Equal(0, srafa.PatternAngle, 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlnaVyplnZustavaSolidBezVzoru()
|
||||
{
|
||||
// md_sfdi má všechny šrafy vzorované, plné výplně jsou v ostatních logách.
|
||||
var dxf = new DxfProfileRenderer();
|
||||
new DxfTemplate(TestData.Logo("exprojekt.dxf")).Replay(dxf);
|
||||
DxfWriter.Write(_soubor, dxf.Document, binary: true);
|
||||
var solidni = DxfReader.Read(_soubor).Entities.OfType<Hatch>().ToList();
|
||||
|
||||
Assert.NotEmpty(solidni);
|
||||
foreach (var h in solidni)
|
||||
{
|
||||
Assert.True(h.IsSolid);
|
||||
Assert.Equal("SOLID", h.Pattern?.Name);
|
||||
Assert.Empty(h.Pattern!.Lines);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using Rozpisky.Dxf;
|
||||
using Rozpisky.Rendering;
|
||||
using Xunit;
|
||||
|
||||
namespace Rozpisky.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Šrafy se vzorem. Logo <c>md_sfdi.dxf</c> má 3× vzor ANSI31 (šikmé čáry); dřív se každá
|
||||
/// šrafa kreslila jako plná výplň, takže z něj byl v náhledu, PNG, PDF i DXF černý blok.
|
||||
/// </summary>
|
||||
public class SrafaVzorTesty
|
||||
{
|
||||
private static RecordingRenderer Prehraj(string logo)
|
||||
{
|
||||
var g = new RecordingRenderer();
|
||||
new DxfTemplate(TestData.Logo(logo)).Replay(g);
|
||||
return g;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ansi31SeRozvineNaCary()
|
||||
{
|
||||
var g = Prehraj("md_sfdi.dxf");
|
||||
var vzorovane = g.Vyplne.Where(v => !v.JeSolid).ToList();
|
||||
|
||||
Assert.Equal(3, vzorovane.Count);
|
||||
foreach (var v in vzorovane)
|
||||
{
|
||||
Assert.Equal("ANSI31", v.Vzor);
|
||||
Assert.NotNull(v.Cary);
|
||||
Assert.NotEmpty(v.Cary!);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CaryLeziUvnitrObrysuSrafy()
|
||||
{
|
||||
var g = Prehraj("md_sfdi.dxf");
|
||||
for (int i = 0; i < g.Vyplne.Count; i++)
|
||||
{
|
||||
if (g.Vyplne[i].JeSolid) continue;
|
||||
var (minX, minY, maxX, maxY) = Bbox(g.Fills[i]);
|
||||
foreach (var u in g.Vyplne[i].Cary!)
|
||||
{
|
||||
Assert.InRange(u.X1, minX - 0.01, maxX + 0.01);
|
||||
Assert.InRange(u.Y1, minY - 0.01, maxY + 0.01);
|
||||
Assert.InRange(u.X2, minX - 0.01, maxX + 0.01);
|
||||
Assert.InRange(u.Y2, minY - 0.01, maxY + 0.01);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CaryAnsi31JsouRovnobezneA45Stupnu()
|
||||
{
|
||||
var g = Prehraj("md_sfdi.dxf");
|
||||
var cary = g.Vyplne.Where(v => !v.JeSolid).SelectMany(v => v.Cary!).ToList();
|
||||
Assert.NotEmpty(cary);
|
||||
|
||||
foreach (var u in cary)
|
||||
{
|
||||
double dx = u.X2 - u.X1, dy = u.Y2 - u.Y1;
|
||||
if (Math.Abs(dx) + Math.Abs(dy) < 1e-9) continue;
|
||||
// ANSI31 je 45° (v tomhle logu 45,007° – tak to má zapsané zdroj).
|
||||
double uhel = Math.Atan2(dy, dx) * 180 / Math.PI;
|
||||
Assert.InRange(uhel, 44.9, 45.1);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlnaSrafaZustavaPlna()
|
||||
{
|
||||
// Ostatní loga mají výhradně SOLID – nesmí se z nich stát čárová šrafa.
|
||||
var g = Prehraj("exprojekt.dxf");
|
||||
Assert.NotEmpty(g.Vyplne);
|
||||
Assert.All(g.Vyplne, v =>
|
||||
{
|
||||
Assert.True(v.JeSolid);
|
||||
Assert.Null(v.Cary);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VychoziVyplnJePlna()
|
||||
{
|
||||
// Pojistka: `default(Vypln)` musí být plná výplň, jinak by volání Fill(loops) bez
|
||||
// druhého parametru přestalo cokoli kreslit.
|
||||
Assert.True(default(Vypln).JeSolid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BezDefiniceVzoruSeNevrationZadneCary()
|
||||
{
|
||||
var ctverec = new[]
|
||||
{
|
||||
new Contour(new[]
|
||||
{
|
||||
Edge.Line(0, 0, 10, 0), Edge.Line(10, 0, 10, 10),
|
||||
Edge.Line(10, 10, 0, 10), Edge.Line(0, 10, 0, 0),
|
||||
}, Closed: true),
|
||||
};
|
||||
Assert.Empty(SrafaVzor.Cary(null, ctverec));
|
||||
}
|
||||
|
||||
private static (double MinX, double MinY, double MaxX, double MaxY) Bbox(
|
||||
IReadOnlyList<IReadOnlyList<(double X, double Y)>> smycky)
|
||||
{
|
||||
double minX = double.MaxValue, minY = double.MaxValue, maxX = double.MinValue, maxY = double.MinValue;
|
||||
foreach (var s in smycky)
|
||||
foreach (var (x, y) in s)
|
||||
{
|
||||
if (x < minX) minX = x;
|
||||
if (y < minY) minY = y;
|
||||
if (x > maxX) maxX = x;
|
||||
if (y > maxY) maxY = y;
|
||||
}
|
||||
return (minX, minY, maxX, maxY);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,9 @@ public static class TestData
|
||||
/// <summary>Plná cesta k logu ve zdrojové složce <c>Rozpisky\Podklady\loga</c>.</summary>
|
||||
public static string Logo(string fileName) => Path.Combine(RepoRoot, "Rozpisky", "Podklady", "loga", fileName);
|
||||
|
||||
/// <summary>Plná cesta k DXF šabloně rozpisky.</summary>
|
||||
public static string Sablona => Path.Combine(RepoRoot, "Rozpisky", "Podklady", "Rozpiska.dxf");
|
||||
|
||||
/// <summary>Všech 9 log dodávaných s programem.</summary>
|
||||
public static readonly string[] VsechnaLoga =
|
||||
{
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
using System.IO;
|
||||
using ACadSharp.Entities;
|
||||
using ACadSharp.IO;
|
||||
using Rozpisky.Dxf;
|
||||
using Rozpisky.Rendering;
|
||||
using Xunit;
|
||||
|
||||
namespace Rozpisky.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Textové styly a MTEXT tam a zpět. Šablona má styly pojmenované česky („Popis základní",
|
||||
/// „Název stavby"); export je dřív zahodil a vyrobil náhradní jména podle fontu (Verdana_1),
|
||||
/// takže ve správci stylů v CADu nešlo nic najít ani hromadně změnit.
|
||||
/// </summary>
|
||||
public class TextStylyRoundTripTesty : IDisposable
|
||||
{
|
||||
private readonly string _soubor = Path.Combine(Path.GetTempPath(), "rozpisky-styly-" + Guid.NewGuid().ToString("N") + ".dxf");
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { File.Delete(_soubor); } catch (IOException) { }
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private ACadSharp.CadDocument Kolecko(Action<IProfileRenderer> kresba)
|
||||
{
|
||||
var dxf = new DxfProfileRenderer();
|
||||
kresba(dxf);
|
||||
DxfWriter.Write(_soubor, dxf.Document, binary: true);
|
||||
return DxfReader.Read(_soubor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StylySiDrziJmenaZeZdroje()
|
||||
{
|
||||
var doc = Kolecko(g => new DxfTemplate(TestData.Sablona).Replay(g));
|
||||
var jmena = doc.TextStyles.Select(s => s.Name).ToList();
|
||||
|
||||
Assert.Contains("Popis základní", jmena);
|
||||
Assert.Contains("Název stavby", jmena);
|
||||
// Náhradní jména podle fontu nesmí vzniknout, když zdroj styl pojmenovaný má.
|
||||
Assert.DoesNotContain(jmena, j => j.StartsWith("Verdana", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JedenZdrojovyStylDaJedenStylVeVystupu()
|
||||
{
|
||||
// Dřív vznikal nový styl pro každou kombinaci fontu a sklonu, takže jich ve výstupu
|
||||
// bylo víc než ve zdroji a texty se rozpadly mezi ně.
|
||||
var zdroj = DxfReader.Read(TestData.Sablona);
|
||||
var doc = Kolecko(g => new DxfTemplate(TestData.Sablona).Replay(g));
|
||||
|
||||
var pouzite = doc.Entities.OfType<TextEntity>().Select(t => t.Style?.Name)
|
||||
.Concat(doc.Entities.OfType<MText>().Select(m => m.Style?.Name))
|
||||
.Where(n => n is not null).Distinct().ToList();
|
||||
|
||||
Assert.NotEmpty(pouzite);
|
||||
foreach (var n in pouzite)
|
||||
Assert.True(zdroj.TextStyles.Contains(n!), $"styl {n} ve zdroji není");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MtextSiDrziNatoceni()
|
||||
{
|
||||
var doc = Kolecko(g => g.Text(0, 0, "dva\nřádky", 3, maxWidthMm: 40, rotationDeg: 30));
|
||||
var mt = Assert.Single(doc.Entities.OfType<MText>());
|
||||
Assert.Equal(30, mt.Rotation * 180 / Math.PI, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MtextSiDrziSirkovyFaktor()
|
||||
{
|
||||
var doc = Kolecko(g => g.Text(0, 0, "text", 3, maxWidthMm: 40, widthFactor: 0.8));
|
||||
var mt = Assert.Single(doc.Entities.OfType<MText>());
|
||||
Assert.StartsWith(@"\W0.8;", mt.Value);
|
||||
|
||||
// A vlastní čtečka ho musí zase odloupnout, jinak by se „W0.8;" vykreslilo jako text.
|
||||
var (cisty, wf) = DxfTemplate.MTextSirka(mt.Value, mt.PlainText);
|
||||
Assert.Equal("text", cisty);
|
||||
Assert.Equal(0.8, wf!.Value, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZpetneLomitkoAZavorkyNerozbijiMtext()
|
||||
{
|
||||
// Reálná hodnota z pole: cesta nebo poznámka se zpětným lomítkem. Bez escapování
|
||||
// by ji CAD přečetl jako formátovací kód a část textu by zmizela.
|
||||
const string hodnota = @"C:\projekt {2025} \P";
|
||||
var doc = Kolecko(g => g.Text(0, 0, hodnota, 3, maxWidthMm: 60));
|
||||
var mt = Assert.Single(doc.Entities.OfType<MText>());
|
||||
Assert.Equal(hodnota, mt.PlainText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZlomRadkuSePreloziNaMtextovyKod()
|
||||
{
|
||||
var doc = Kolecko(g => g.Text(0, 0, "první\ndruhý", 3, maxWidthMm: 60));
|
||||
var mt = Assert.Single(doc.Entities.OfType<MText>());
|
||||
Assert.Contains(@"\P", mt.Value);
|
||||
Assert.Equal("první\ndruhý", mt.PlainText.Replace("\r\n", "\n"));
|
||||
}
|
||||
}
|
||||
@@ -164,11 +164,17 @@ public static class CiselnikyStore
|
||||
};
|
||||
|
||||
/// <summary>Dodávaná vrstva → odebrat skryté → přepsat/doplnit vlastními. Pořadí dodávaných
|
||||
/// položek zůstane zachované, vlastní se řadí za ně.</summary>
|
||||
/// položek zůstane zachované, vlastní se řadí za ně.
|
||||
/// <para>
|
||||
/// Dodávané položky se do cíle vkládají jako <b>kopie</b>. Kdyby se sdílely s referenčním
|
||||
/// snímkem <c>_vychozi</c>, úprava v mřížce by mutovala i ten snímek – rozdíl by se pak počítal
|
||||
/// proti už změněným datům, vyšel by prázdný a uživatelova oprava dodávané položky by se tiše
|
||||
/// ztratila (mazání a přidávání by přitom fungovalo, což tu chybu dobře schová).
|
||||
/// </para></summary>
|
||||
internal static void Sluc<T>(ObservableCollection<T> cil, List<T> vychozi, Vrstva<T>? uziv)
|
||||
{
|
||||
var skryte = new HashSet<string>(uziv?.Skryte ?? new List<string>(), StringComparer.OrdinalIgnoreCase);
|
||||
var vysledek = vychozi.Where(x => !skryte.Contains(Klic(x!))).ToList();
|
||||
var vysledek = vychozi.Where(x => !skryte.Contains(Klic(x!))).Select(Klon).ToList();
|
||||
|
||||
foreach (var vlastni in uziv?.Vlastni ?? new List<T>())
|
||||
{
|
||||
@@ -215,6 +221,11 @@ public static class CiselnikyStore
|
||||
private static bool StejnyObsah<T>(T a, T b)
|
||||
=> JsonSerializer.Serialize(a, Opts) == JsonSerializer.Serialize(b, Opts);
|
||||
|
||||
/// <summary>Hluboká kopie položky (přes serializaci, stejně jako to dělá klonování přílohy).
|
||||
/// Drží oddělený referenční snímek dodávané vrstvy od objektů, které edituje uživatel.</summary>
|
||||
private static T Klon<T>(T polozka)
|
||||
=> JsonSerializer.Deserialize<T>(JsonSerializer.Serialize(polozka, Opts), Opts)!;
|
||||
|
||||
private static void Replace<T>(ObservableCollection<T> target, IEnumerable<T> src)
|
||||
{
|
||||
target.Clear();
|
||||
|
||||
+253
-18
@@ -86,12 +86,34 @@ public sealed partial class DxfTemplate
|
||||
/// (např. A4 rámeček) – pro DXF export jako referenci hranice papíru.</summary>
|
||||
public void Replay(IProfileRenderer g, IReadOnlyDictionary<string, string>? values = null, bool includeNoPrint = false)
|
||||
{
|
||||
// Jeden průchod v pořadí kresby (viz EntitiesInDrawOrder) – zachovává překrývání entit ze
|
||||
// zdroje (výplně vzadu, popisky vepředu apod.). Dispatch podle typu; odvozené typy (Arc:Circle,
|
||||
// AttributeDefinition:AttributeBase:TextEntity) musí být ve switchi PŘED svými předky.
|
||||
foreach (var e in EntitiesInDrawOrder())
|
||||
_nezpracovane.Clear();
|
||||
Prehraj(g, EntitiesInDrawOrder(), values, includeNoPrint, vlozeny: null, hloubka: 0);
|
||||
}
|
||||
|
||||
/// <summary>Počty entit, které kreslítko neumí, podle typu (např. <c>DIMENSION</c> → 3).
|
||||
/// Naplní se při každém <see cref="Replay"/>. Prázdné = vykreslilo se všechno. Slouží
|
||||
/// k upozornění uživatele, když si přidá logo s prvky, které program nezná – dřív takové
|
||||
/// prvky mizely tiše.</summary>
|
||||
public IReadOnlyDictionary<string, int> NezpracovaneEntity => _nezpracovane;
|
||||
|
||||
private readonly Dictionary<string, int> _nezpracovane = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>Nejhlubší zanoření bloků, do kterého se ještě jde. Chrání před blokem, který
|
||||
/// (chybou ve zdroji) vkládá sám sebe.</summary>
|
||||
private const int MaxHloubkaBloku = 8;
|
||||
|
||||
/// <summary>Jeden průchod v pořadí kresby (viz <see cref="EntitiesInDrawOrder"/>) – zachovává
|
||||
/// překrývání entit ze zdroje (výplně vzadu, popisky vepředu). Dispatch podle typu; odvozené
|
||||
/// typy (Arc:Circle, AttributeDefinition:AttributeBase:TextEntity) musí být ve switchi PŘED
|
||||
/// svými předky. <paramref name="vlozeny"/> = stav vkládajícího <c>INSERT</c>u při rozvíjení
|
||||
/// bloku (řeší barvy ByBlock a hladinu „0"), null v model space.</summary>
|
||||
private void Prehraj(IProfileRenderer g, IEnumerable<Entity> entity,
|
||||
IReadOnlyDictionary<string, string>? values, bool includeNoPrint,
|
||||
KresliciStav? vlozeny, int hloubka)
|
||||
{
|
||||
foreach (var e in entity)
|
||||
{
|
||||
if (!SetLayer(g, e, includeNoPrint)) continue;
|
||||
if (!SetLayer(g, e, includeNoPrint, vlozeny)) continue;
|
||||
switch (e)
|
||||
{
|
||||
case Line l:
|
||||
@@ -110,7 +132,7 @@ public sealed partial class DxfTemplate
|
||||
// Spliny (NURBS) – tesselace na lomenou čáru přes ACadSharp.
|
||||
case Spline sp:
|
||||
{
|
||||
var pts = sp.PolygonalVertexes(64).Select(p => (p.X, p.Y)).ToList();
|
||||
var pts = sp.PolygonalVertexes(DilkuSplinu(sp)).Select(p => (p.X, p.Y)).ToList();
|
||||
if (pts.Count >= 2) g.Polyline(pts, sp.IsClosed);
|
||||
break;
|
||||
}
|
||||
@@ -128,7 +150,7 @@ public sealed partial class DxfTemplate
|
||||
case Hatch hatch:
|
||||
{
|
||||
var loops = HatchSmycky(hatch);
|
||||
if (loops.Count > 0) g.Fill(loops);
|
||||
if (loops.Count > 0) g.Fill(loops, VyplnZ(hatch, loops));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -166,14 +188,115 @@ public sealed partial class DxfTemplate
|
||||
case MText mt:
|
||||
{
|
||||
var (h, v) = MapMTextAlign(mt.AttachmentPoint);
|
||||
g.Text(mt.InsertPoint.X, mt.InsertPoint.Y, mt.PlainText, mt.Height, h, v, mt.Rotation * RadToDeg,
|
||||
StyleOf(mt.Style), 1, mt.RectangleWidth);
|
||||
// MTEXT nemá šířkový faktor jako vlastnost entity – nese ho buď kód \W
|
||||
// na začátku hodnoty, nebo (jinak) styl textu.
|
||||
var (text, wfKod) = MTextSirka(mt.Value, mt.PlainText);
|
||||
double wf = wfKod ?? (mt.Style?.Width > 0 ? mt.Style.Width : 1);
|
||||
g.Text(mt.InsertPoint.X, mt.InsertPoint.Y, text, mt.Height, h, v, mt.Rotation * RadToDeg,
|
||||
StyleOf(mt.Style), wf, mt.RectangleWidth);
|
||||
break;
|
||||
}
|
||||
|
||||
// Vložený blok – rozvine se rekurzivně. Bez toho by se uživatelovo logo s blokem
|
||||
// nevykreslilo vůbec a bez hlášky.
|
||||
case Insert ins:
|
||||
RozvinBlok(g, ins, values, includeNoPrint, hloubka);
|
||||
break;
|
||||
|
||||
// Elipsa: rozhraní zná jen kruhové oblouky, takže se navzorkuje na lomenou čáru.
|
||||
case Ellipse el:
|
||||
{
|
||||
var body = VzorkujEllipse(el);
|
||||
if (body.Count >= 2) g.Polyline(body, el.IsFullEllipse);
|
||||
break;
|
||||
}
|
||||
|
||||
// Bod – kreslí se jako tečka (úsečka nulové délky, kulaté zakončení čáry).
|
||||
case Point p:
|
||||
g.Line(p.Location.X, p.Location.Y, p.Location.X, p.Location.Y);
|
||||
break;
|
||||
|
||||
// Vyplněný čtyřúhelník. Pozor: v DXF jsou 3. a 4. roh prohozené proti obvodu.
|
||||
case Solid s:
|
||||
KresliCtyruhelnik(g, s.FirstCorner.X, s.FirstCorner.Y, s.SecondCorner.X, s.SecondCorner.Y,
|
||||
s.ThirdCorner.X, s.ThirdCorner.Y, s.FourthCorner.X, s.FourthCorner.Y);
|
||||
break;
|
||||
|
||||
case Face3D f:
|
||||
KresliCtyruhelnik(g, f.FirstCorner.X, f.FirstCorner.Y, f.SecondCorner.X, f.SecondCorner.Y,
|
||||
f.ThirdCorner.X, f.ThirdCorner.Y, f.FourthCorner.X, f.FourthCorner.Y);
|
||||
break;
|
||||
|
||||
// Prostorová polylinie – promítne se do roviny XY (kresba je 2D).
|
||||
case Polyline3D pl3:
|
||||
{
|
||||
var body = pl3.Vertices.Select(v => (v.Location.X, v.Location.Y)).ToList();
|
||||
if (body.Count >= 2) g.Polyline(body, pl3.IsClosed);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
Nezpracovano(e.ObjectName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Rozvine <c>INSERT</c>: entity bloku se přehrají přes transformaci (posun na
|
||||
/// vkládací bod, měřítko os, natočení). Barvy <c>ByBlock</c> a hladina „0" uvnitř bloku
|
||||
/// přebírají stav vkládající entity – tak to dělá i AutoCAD.</summary>
|
||||
private void RozvinBlok(IProfileRenderer g, Insert ins,
|
||||
IReadOnlyDictionary<string, string>? values, bool includeNoPrint, int hloubka)
|
||||
{
|
||||
if (hloubka >= MaxHloubkaBloku) { Nezpracovano("INSERT (příliš zanořený)"); return; }
|
||||
|
||||
var blok = ins.Block;
|
||||
if (blok is null) { Nezpracovano("INSERT (chybí definice bloku)"); return; }
|
||||
|
||||
var zaklad = blok.BlockEntity?.BasePoint ?? default;
|
||||
var transformace = TransformRenderer.ProBlok(
|
||||
g, ins.InsertPoint.X, ins.InsertPoint.Y, zaklad.X, zaklad.Y,
|
||||
ins.XScale, ins.YScale, ins.Rotation);
|
||||
|
||||
var stavVkladajici = new KresliciStav(HladinaZ(ins.Layer), BarvaZ(ins.Color),
|
||||
ins.LineType?.Name, TloustkaZ(ins.LineWeight));
|
||||
Prehraj(transformace, blok.Entities, values, includeNoPrint, stavVkladajici, hloubka + 1);
|
||||
}
|
||||
|
||||
/// <summary>Vyplněný čtyřúhelník ze čtyř rohů v pořadí DXF (3. a 4. roh prohozený).
|
||||
/// Trojúhelník se pozná podle shodného 3. a 4. rohu.</summary>
|
||||
private static void KresliCtyruhelnik(IProfileRenderer g, double x1, double y1, double x2, double y2,
|
||||
double x3, double y3, double x4, double y4)
|
||||
{
|
||||
var hrany = new List<Edge> { Edge.Line(x1, y1, x2, y2), Edge.Line(x2, y2, x4, y4) };
|
||||
if (Math.Abs(x3 - x4) > 1e-9 || Math.Abs(y3 - y4) > 1e-9)
|
||||
hrany.Add(Edge.Line(x4, y4, x3, y3));
|
||||
hrany.Add(Edge.Line(hrany[^1].EX, hrany[^1].EY, x1, y1));
|
||||
g.Fill(new[] { new Contour(hrany, Closed: true) });
|
||||
}
|
||||
|
||||
/// <summary>Na kolik dílků rozložit spline. Pevných 64 bylo málo pro dlouhé křivky (viditelné
|
||||
/// hrany) a zbytečně moc pro krátké; počítá se z délky řídicího polygonu tak, aby jeden dílek
|
||||
/// vyšel zhruba na 0,3 mm výkresu.</summary>
|
||||
private static int DilkuSplinu(Spline sp)
|
||||
{
|
||||
const double DilekMm = 0.3;
|
||||
double delka = 0;
|
||||
var body = sp.ControlPoints;
|
||||
for (int i = 1; i < body.Count; i++)
|
||||
{
|
||||
double dx = body[i].X - body[i - 1].X, dy = body[i].Y - body[i - 1].Y;
|
||||
delka += Math.Sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
return Math.Clamp((int)Math.Ceiling(delka / DilekMm), 16, 512);
|
||||
}
|
||||
|
||||
private void Nezpracovano(string typ)
|
||||
=> _nezpracovane[typ] = _nezpracovane.GetValueOrDefault(typ) + 1;
|
||||
|
||||
/// <summary>Tloušťka čáry v mm z DXF hodnoty (setiny mm); záporná = podle hladiny → null.</summary>
|
||||
private static double? TloustkaZ(LineWeightType lw) => (int)lw > 0 ? (int)lw / 100.0 : null;
|
||||
|
||||
/// <summary>Entity model-space v pořadí kresby: pokud má blok tabulku pořadí (<c>SortEntitiesTable</c>,
|
||||
/// tj. „draw order" nastavené v CADu), seřadí podle ní; jinak zachová pořadí ze souboru. Nižší sort
|
||||
/// handle = kreslí se dřív (vzadu). LINQ <c>OrderBy</c> je stabilní → entity bez záznamu drží pořadí.</summary>
|
||||
@@ -204,20 +327,108 @@ public sealed partial class DxfTemplate
|
||||
/// <summary>Nastaví hladinu do kreslítka. Vrací false, pokud se hladina nemá tisknout (přeskočit kresbu).
|
||||
/// <paramref name="includeNoPrint"/> = true → geometrie na <see cref="HladinaNoPrint"/> se nepřeskočí
|
||||
/// (kreslí se i přes vypnutý plot flag – DXF reference A4 rámečku).</summary>
|
||||
private static bool SetLayer(IProfileRenderer g, Entity e, bool includeNoPrint = false)
|
||||
private static bool SetLayer(IProfileRenderer g, Entity e, bool includeNoPrint = false,
|
||||
KresliciStav? vlozeny = null)
|
||||
{
|
||||
var layer = e.Layer;
|
||||
bool noPrint = JeNoPrint(e);
|
||||
if (noPrint && !includeNoPrint) return false; // vodicí rámeček vkládaného DXF se nekreslí
|
||||
if (!noPrint && layer is not null && (!layer.PlotFlag || !layer.IsOn)) return false;
|
||||
var col = e.Color.IsByLayer && layer is not null ? layer.Color : e.Color; // ByLayer → barva hladiny
|
||||
int rgb = (col.R << 16) | (col.G << 8) | col.B; // ACadSharp rozbalí i index/true-color
|
||||
int lw = (int)e.LineWeight; // LineWeightType: Wnn = nn/100 mm, záporné = ByLayer/Default/ByBlock
|
||||
if (lw < 0 && layer is not null) lw = (int)layer.LineWeight;
|
||||
g.Layer(layer?.Name ?? "0", rgb, lw > 0 ? lw / 100.0 : 0.18);
|
||||
|
||||
var hladina = HladinaZ(layer);
|
||||
var barva = BarvaZ(e.Color);
|
||||
var tloustka = TloustkaZ(e.LineWeight);
|
||||
var typCary = e.LineType?.Name;
|
||||
|
||||
// Uvnitř bloku: hladina „0" a barva/typ čáry ByBlock se berou od vkládající entity –
|
||||
// právě proto se stejný blok dá vložit dvakrát a pokaždé v jiné barvě.
|
||||
if (vlozeny is { } v)
|
||||
{
|
||||
if (hladina.Nazev is null or "0") hladina = v.Hladina;
|
||||
if (barva.Druh == BarvaDruh.ByBlock) barva = v.Barva;
|
||||
tloustka ??= v.TloustkaMm;
|
||||
if (typCary is null or "ByBlock") typCary = v.TypCary;
|
||||
}
|
||||
|
||||
g.Stav(new KresliciStav(hladina, barva, typCary, tloustka));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Odloupne z MTEXTu úvodní kód šířkového faktoru <c>\W1.2;</c>. ACadSharp ho
|
||||
/// v <c>PlainText</c> nechává (jen bez zpětného lomítka), takže by se jinak vykreslil jako
|
||||
/// text. Vrací čistý text a faktor, když tam byl.</summary>
|
||||
internal static (string Text, double? WidthFactor) MTextSirka(string? hodnota, string? plainText)
|
||||
{
|
||||
string plain = plainText ?? "";
|
||||
if (hodnota is null || !hodnota.StartsWith(@"\W", StringComparison.Ordinal)) return (plain, null);
|
||||
int strednik = hodnota.IndexOf(';', 2);
|
||||
if (strednik < 0) return (plain, null);
|
||||
if (!double.TryParse(hodnota[2..strednik], System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out double wf) || wf <= 0)
|
||||
return (plain, null);
|
||||
|
||||
// Z PlainText ubyla jen zpětná lomítka, tedy „W1.2;" bez prvního znaku kódu.
|
||||
string predpona = hodnota[1..(strednik + 1)];
|
||||
if (plain.StartsWith(predpona, StringComparison.Ordinal)) plain = plain[predpona.Length..];
|
||||
return (plain, wf);
|
||||
}
|
||||
|
||||
/// <summary>Druh výplně pro HATCH. Plná (SOLID) projde beze změny; u vzoru se vzor rozvine
|
||||
/// na čáry pro rastrové renderery, ale jméno vzoru, měřítko a úhel se nesou dál, aby DXF
|
||||
/// export zapsal zase editovatelnou šrafu. Když se vzor rozvinout nepodaří, plocha zůstane
|
||||
/// prázdná – dřív se v takovém případě kreslila plná výplň a z logotypu byl černý blok.</summary>
|
||||
private static Vypln VyplnZ(Hatch hatch, IReadOnlyList<Contour> smycky)
|
||||
{
|
||||
string vzor = hatch.Pattern?.Name ?? "SOLID";
|
||||
if (hatch.IsSolid || string.IsNullOrWhiteSpace(vzor)
|
||||
|| vzor.Equals("SOLID", StringComparison.OrdinalIgnoreCase))
|
||||
return default;
|
||||
|
||||
var definice = DefiniceVzoru(hatch.Pattern);
|
||||
return Vypln.Vzorovana(vzor, hatch.PatternScale, hatch.PatternAngle * RadToDeg,
|
||||
definice, SrafaVzor.Cary(definice, smycky));
|
||||
}
|
||||
|
||||
/// <summary>Definiční čáry vzoru šrafy z entity. DXF je nese uvnitř <c>HATCH</c> (group 78 a dál),
|
||||
/// tedy už v efektivních hodnotách – měřítko a úhel z vlastností šrafy jsou v nich započítané.</summary>
|
||||
private static List<VzorCara> DefiniceVzoru(HatchPattern? vzor)
|
||||
{
|
||||
var vysledek = new List<VzorCara>();
|
||||
foreach (var cara in vzor?.Lines ?? new List<HatchPattern.Line>())
|
||||
vysledek.Add(new VzorCara(
|
||||
cara.Angle * RadToDeg,
|
||||
(cara.BasePoint.X, cara.BasePoint.Y),
|
||||
(cara.Offset.X, cara.Offset.Y),
|
||||
cara.DashLengths?.ToList() ?? new List<double>()));
|
||||
return vysledek;
|
||||
}
|
||||
|
||||
/// <summary>Popis hladiny pro kreslítko – celý, ať se dá ve výstupu postavit znovu
|
||||
/// (barva z tabulky hladin, tloušťka, typ čáry, příznak tisku).</summary>
|
||||
private static HladinaStav HladinaZ(ACadSharp.Tables.Layer? layer)
|
||||
{
|
||||
if (layer is null) return HladinaStav.Vychozi;
|
||||
int lw = (int)layer.LineWeight;
|
||||
return new HladinaStav(
|
||||
layer.Name,
|
||||
BarvaZ(layer.Color),
|
||||
layer.LineType?.Name ?? "Continuous",
|
||||
lw > 0 ? lw / 100.0 : 0,
|
||||
layer.PlotFlag,
|
||||
layer.IsOn);
|
||||
}
|
||||
|
||||
/// <summary>Barva z DXF i s původem: index (group 62), true color (420) nebo odkaz
|
||||
/// ByLayer/ByBlock. Původ se nese dál, aby ho DXF export mohl zapsat beze změny –
|
||||
/// zplošťování na RGB dřív z indexovaných barev dělalo natvrdo RGB.</summary>
|
||||
private static Barva BarvaZ(ACadSharp.Color c)
|
||||
{
|
||||
if (c.IsByLayer) return Barva.ZHladiny;
|
||||
if (c.IsByBlock) return Barva.ZBloku;
|
||||
if (c.IsTrueColor) return Barva.Prima((c.R << 16) | (c.G << 8) | c.B);
|
||||
return c.Index is >= 1 and <= 255 ? Barva.Aci(c.Index) : Barva.Aci(7);
|
||||
}
|
||||
|
||||
/// <summary>Zjistí kotvu a zarovnání textu (Left+Baseline → vkládací bod, jinak zarovnávací bod).</summary>
|
||||
private static (double x, double y, HAlign h, VAlign v) Place(
|
||||
TextHorizontalAlignment ha, TextVerticalAlignmentType va, XYZ insert, XYZ align)
|
||||
@@ -248,13 +459,14 @@ public sealed partial class DxfTemplate
|
||||
{
|
||||
double obliqueRad = entityObliqueRad != 0 ? entityObliqueRad : s?.ObliqueAngle ?? 0;
|
||||
if (s is null) return new(ObliqueDeg: obliqueRad * RadToDeg);
|
||||
string? file = s.IsShapeFile || string.IsNullOrWhiteSpace(s.Filename) ? null : s.Filename;
|
||||
string? file = string.IsNullOrWhiteSpace(s.Filename) ? null : s.Filename;
|
||||
string? ttf = s.IsShapeFile ? null : file;
|
||||
bool bold = s.TrueType.HasFlag(FontFlags.Bold);
|
||||
bool italic = s.TrueType.HasFlag(FontFlags.Italic);
|
||||
// Rodina písma: z .ttf souboru; když styl soubor nemá (TTF zadané jen jménem), vezmi
|
||||
// název typu písma z XData(ACAD) – tam AutoCAD ukládá rodinu (např. „Arial Black").
|
||||
string? family = file is not null ? Path.GetFileNameWithoutExtension(file) : FontRodinaZeXData(s);
|
||||
return new(family, bold, italic, file, obliqueRad * RadToDeg);
|
||||
string? family = ttf is not null ? Path.GetFileNameWithoutExtension(ttf) : FontRodinaZeXData(s);
|
||||
return new(family, bold, italic, file, obliqueRad * RadToDeg, s.Name, s.IsShapeFile);
|
||||
}
|
||||
|
||||
/// <summary>Název rodiny TrueType písma uložený ve stylu jako XData appid „ACAD" (první ASCII
|
||||
@@ -555,6 +767,29 @@ public sealed partial class DxfTemplate
|
||||
return pts;
|
||||
}
|
||||
|
||||
/// <summary>Navzorkuje entitu ELLIPSE na body. Kreslicí rozhraní zná jen kruhové oblouky,
|
||||
/// takže elipsa jde do lomené čáry – dělení podle hlavní poloosy, aby byla dost hladká.
|
||||
/// Parametry (nikoli úhly) jdou vždy proti směru hodin od <c>StartParameter</c>.</summary>
|
||||
private static List<(double X, double Y)> VzorkujEllipse(Ellipse el)
|
||||
{
|
||||
double sa = el.StartParameter, ea = el.EndParameter;
|
||||
if (ea <= sa) ea += 2 * Math.PI;
|
||||
double sweep = ea - sa;
|
||||
|
||||
double mx = el.MajorAxisEndPoint.X, my = el.MajorAxisEndPoint.Y;
|
||||
double vx = -my * el.RadiusRatio, vy = mx * el.RadiusRatio; // vedlejší poloosa = kolmice·ratio
|
||||
double major = Math.Sqrt(mx * mx + my * my);
|
||||
|
||||
int seg = Math.Max(8, ContourGeometry.ArcSegments(sweep, Math.Max(major, 1e-9), 0.02));
|
||||
var pts = new List<(double X, double Y)>(seg + 1);
|
||||
for (int i = 0; i <= seg; i++)
|
||||
{
|
||||
double t = sa + sweep * i / seg, c = Math.Cos(t), s = Math.Sin(t);
|
||||
pts.Add((el.Center.X + c * mx + s * vx, el.Center.Y + c * my + s * vy));
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
/// <summary>Navzorkuje eliptický oblouk hatch hranice na body v OCS (kolmice·ratio, CW přes osu X).</summary>
|
||||
private static List<(double X, double Y)> VzorkujEllipse(Hatch.BoundaryPath.Ellipse el)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using Rozpisky.Data;
|
||||
using Rozpisky.Models;
|
||||
|
||||
namespace Rozpisky.Dxf;
|
||||
@@ -8,19 +9,67 @@ namespace Rozpisky.Dxf;
|
||||
/// Pravidla (kap. 4.3 manuálu SŽ):
|
||||
/// <list type="bullet">
|
||||
/// <item><c>Rozpiska_logo1/2</c> – financující instituce (dotační vlevo / státní vpravo),
|
||||
/// projektové, soubor ze složky <c>loga\</c> vedle .exe,</item>
|
||||
/// projektové, soubor ze složky <c>loga\</c>,</item>
|
||||
/// <item><c>Rozpiska_logo3</c> – logo investora (číselník Organizace dle <c>Projekt.Investor</c>),</item>
|
||||
/// <item><c>Rozpiska_logo4</c> – logo zhotovitele díla (číselník Firmy dle <c>Projekt.ZhotovitelSOD</c>),</item>
|
||||
/// <item><c>Rozpiska_logo5</c> – logo zhotovitele objektu (číselník Firmy dle <c>Priloha.ZhotovitelCasti</c>),</item>
|
||||
/// <item><c>Rozpiska_Orientační schéma</c> – DXF u přílohy (<c>Priloha.SchemaSoubor</c>), relativně k .rzp.</item>
|
||||
/// </list>
|
||||
/// Položky bez vyplněného / neexistujícího souboru se vynechají.
|
||||
/// <para>
|
||||
/// Loga se hledají ve <b>dvou složkách</b> se stejnou logikou jako číselníky: nejdřív
|
||||
/// <c>loga\</c> v uživatelské složce (viz <see cref="UzivatelskaData"/>), pak <c>loga\</c> vedle
|
||||
/// .exe. Uživatelské logo stejného jména přebíjí dodávané. Bez toho by si uživatel, který přidá
|
||||
/// do číselníku firmu s vlastním logem, musel soubor kopírovat do instalační složky – tu ale
|
||||
/// přeinstalace přepíše a v <c>Program Files</c> do ní nejde psát.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class LogaResolver
|
||||
{
|
||||
/// <summary>Složka s logy dodávanými s programem (vedle .exe).</summary>
|
||||
/// <summary>Složka s logy dodávanými s programem (vedle .exe). Jen ke čtení – instalace ji přepisuje.</summary>
|
||||
public static string LogaDir => Path.Combine(AppContext.BaseDirectory, "loga");
|
||||
|
||||
/// <summary>Složka pro loga, která si přidal uživatel. Přeinstalace na ni nesahá.</summary>
|
||||
public static string UzivatelskaLogaDir => UzivatelskaData.Cesta("loga");
|
||||
|
||||
/// <summary>Najde soubor loga: absolutní cesta se bere, jak je, jinak se hledá nejdřív mezi
|
||||
/// uživatelskými logy a pak mezi dodávanými. Vrací null, když soubor nikde není.</summary>
|
||||
public static string? Najdi(string? soubor)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(soubor)) return null;
|
||||
if (Path.IsPathRooted(soubor)) return File.Exists(soubor) ? soubor : null;
|
||||
|
||||
var uzivatelske = Path.Combine(UzivatelskaLogaDir, soubor);
|
||||
if (File.Exists(uzivatelske)) return uzivatelske;
|
||||
|
||||
var dodavane = Path.Combine(LogaDir, soubor);
|
||||
return File.Exists(dodavane) ? dodavane : null;
|
||||
}
|
||||
|
||||
/// <summary>Jména dostupných log z obou složek dohromady, abecedně. Uživatelské logo stejného
|
||||
/// jména dodávané nezdvojuje – v nabídce je jednou a použije se to uživatelovo.</summary>
|
||||
public static IReadOnlyList<string> DostupnaLoga() => SlozNabidku(UzivatelskaLogaDir, LogaDir);
|
||||
|
||||
/// <summary>Vlastní slučování nabídky – s cestami v parametru, ať jde otestovat bez profilu uživatele.</summary>
|
||||
internal static List<string> SlozNabidku(string uzivatelskaDir, string dodavanaDir)
|
||||
{
|
||||
var jmena = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var dir in new[] { dodavanaDir, uzivatelskaDir })
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(dir)) continue;
|
||||
foreach (var soubor in Directory.GetFiles(dir, "*.dxf"))
|
||||
jmena.Add(Path.GetFileName(soubor));
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
// Nedostupná složka (odpojený síťový disk apod.) nesmí shodit nabídku.
|
||||
}
|
||||
}
|
||||
return jmena.ToList();
|
||||
}
|
||||
|
||||
/// <param name="rzpDir">Složka aktuálního souboru .rzp (pro relativní cestu k schématu); null = neuloženo.</param>
|
||||
public static IReadOnlyDictionary<string, string> Resolve(Projekt p, Priloha r, Ciselniky c, string? rzpDir)
|
||||
{
|
||||
@@ -40,9 +89,8 @@ public static class LogaResolver
|
||||
|
||||
private static void PridejZeSlozky(IDictionary<string, string> d, string hladina, string? soubor)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(soubor)) return;
|
||||
var cesta = Path.IsPathRooted(soubor) ? soubor : Path.Combine(LogaDir, soubor);
|
||||
if (File.Exists(cesta)) d[hladina] = cesta;
|
||||
var cesta = Najdi(soubor);
|
||||
if (cesta is not null) d[hladina] = cesta;
|
||||
}
|
||||
|
||||
/// <summary>Cesta ke schématu: absolutní beze změny, jinak relativně ke složce .rzp.</summary>
|
||||
|
||||
@@ -81,6 +81,28 @@ public sealed class RozpiskaRenderer
|
||||
catch { return null; } // poškozený/nečitelný DXF fragment → bez loga
|
||||
});
|
||||
|
||||
/// <summary>Prvky vložených DXF (log, schémat), které kreslítko neumí – po jednom řádku na
|
||||
/// soubor, např. <c>„nase_logo.dxf: DIMENSION ×3, LEADER ×1"</c>. Prázdné = vykreslí se všechno.
|
||||
/// <para>Uživatel si přidává vlastní loga; bez tohohle by v nich chybějící prvky prostě zmizely
|
||||
/// a nikdo by se to nedozvěděl. Nekreslí se nic – kresba jde do <see cref="BoundsRenderer"/>,
|
||||
/// jen aby se sesbírala statistika.</para></summary>
|
||||
public static IReadOnlyList<string> NepodporovanePrvky(IEnumerable<string> cestyKFragmentum)
|
||||
{
|
||||
var hlaseni = new List<string>();
|
||||
foreach (var cesta in cestyKFragmentum.Where(c => !string.IsNullOrWhiteSpace(c)).Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var frag = NactiFragment(cesta);
|
||||
if (frag is null) continue;
|
||||
|
||||
frag.Replay(new BoundsRenderer());
|
||||
if (frag.NezpracovaneEntity.Count == 0) continue;
|
||||
|
||||
var vypis = string.Join(", ", frag.NezpracovaneEntity.OrderBy(p => p.Key).Select(p => $"{p.Key} ×{p.Value}"));
|
||||
hlaseni.Add($"{Path.GetFileName(cesta)}: {vypis}");
|
||||
}
|
||||
return hlaseni;
|
||||
}
|
||||
|
||||
public void ExportPdf(Projekt p, Priloha r, IEnumerable<Revize> revize, Ciselniky c, string path,
|
||||
IReadOnlyDictionary<string, string>? loga = null)
|
||||
=> Exporters.PdfA4(BuildDraw(p, r, revize, c, vyplnene: true, loga), path, frame: _tpl.SheetFrame);
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
using Rozpisky.Rendering;
|
||||
|
||||
namespace Rozpisky.Dxf;
|
||||
|
||||
/// <summary>
|
||||
/// Rozvine vzor šrafy (ANSI31 a spol.) na čáry. Rastrové renderery vzory neumí – bez tohohle
|
||||
/// se každá šrafa kreslila jako plná výplň a z logotypu <c>md_sfdi.dxf</c> byl černý blok.
|
||||
/// Do DXF se zapisuje původní <c>HATCH</c> se vzorem, tyhle čáry jsou jen pro kresbu.
|
||||
/// </summary>
|
||||
internal static class SrafaVzor
|
||||
{
|
||||
/// <summary>Strop počtu čar na jednu šrafu. Vadné měřítko ve zdroji (nebo šrafa přes celý
|
||||
/// výkres) by jinak vygenerovalo statisíce úseček a náhled by zamrzl.</summary>
|
||||
private const int MaxCar = 20_000;
|
||||
|
||||
/// <summary>Jemnost, s jakou se hranice šrafy zplošťují na mnohoúhelník (mm).</summary>
|
||||
private const double Sagitta = 0.02;
|
||||
|
||||
/// <summary>Čáry vzoru uvnitř zadaných smyček. Prázdné, když vzor není použitelný –
|
||||
/// volající pak plochu nechá bez výplně (lepší než ji zaplácnout načerno).</summary>
|
||||
public static IReadOnlyList<Usecka> Cary(IReadOnlyList<VzorCara>? definice, IReadOnlyList<Contour> smycky)
|
||||
{
|
||||
var vysledek = new List<Usecka>();
|
||||
if (definice is not { Count: > 0 }) return vysledek;
|
||||
|
||||
var polygony = Zplosti(smycky);
|
||||
if (polygony.Count == 0) return vysledek;
|
||||
|
||||
var (minX, minY, maxX, maxY) = Rozsah(polygony);
|
||||
if (maxX - minX <= 0 || maxY - minY <= 0) return vysledek;
|
||||
|
||||
foreach (var cara in definice)
|
||||
{
|
||||
PridejCaryVzoru(cara, polygony, minX, minY, maxX, maxY, vysledek);
|
||||
if (vysledek.Count >= MaxCar) break;
|
||||
}
|
||||
return vysledek;
|
||||
}
|
||||
|
||||
/// <summary>Jedna definiční čára vzoru = celá rodina rovnoběžek. Posun mezi sousedními
|
||||
/// rovnoběžkami je vektor <c>Offset</c> ve světových souřadnicích: jeho kolmá složka určuje
|
||||
/// rozteč, podélná posouvá čárkování. U ANSI31 vychází přesně kolmý na směr 45°.</summary>
|
||||
private static void PridejCaryVzoru(
|
||||
VzorCara cara, List<List<(double X, double Y)>> polygony,
|
||||
double minX, double minY, double maxX, double maxY, List<Usecka> vysledek)
|
||||
{
|
||||
double uhel = cara.UhelDeg * Math.PI / 180;
|
||||
double dx = Math.Cos(uhel), dy = Math.Sin(uhel); // směr čáry
|
||||
double nx = -dy, ny = dx; // kolmice
|
||||
|
||||
double ox = cara.Offset.X, oy = cara.Offset.Y;
|
||||
double kolma = ox * nx + oy * ny; // rozteč rovnoběžek
|
||||
if (Math.Abs(kolma) < 1e-9) return; // rovnoběžky by splynuly
|
||||
|
||||
// Kolik kroků pokryje bbox: promítni jeho rohy na kolmici.
|
||||
double zaklad = cara.Zaklad.X * nx + cara.Zaklad.Y * ny;
|
||||
var projekce = new[]
|
||||
{
|
||||
minX * nx + minY * ny, maxX * nx + minY * ny,
|
||||
minX * nx + maxY * ny, maxX * nx + maxY * ny,
|
||||
};
|
||||
double kOd = (projekce.Min() - zaklad) / kolma;
|
||||
double kDo = (projekce.Max() - zaklad) / kolma;
|
||||
if (kOd > kDo) (kOd, kDo) = (kDo, kOd);
|
||||
|
||||
int od = (int)Math.Floor(kOd) - 1, doK = (int)Math.Ceiling(kDo) + 1;
|
||||
if ((long)doK - od > MaxCar) return; // nesmyslně husté měřítko
|
||||
|
||||
var carky = Carky(cara.Carky);
|
||||
|
||||
for (int k = od; k <= doK; k++)
|
||||
{
|
||||
double px = cara.Zaklad.X + k * ox, py = cara.Zaklad.Y + k * oy;
|
||||
foreach (var (t1, t2) in Uvnitr(polygony, px, py, dx, dy))
|
||||
foreach (var (u1, u2) in Orez(carky, t1, t2))
|
||||
{
|
||||
vysledek.Add(new Usecka(px + dx * u1, py + dy * u1, px + dx * u2, py + dy * u2));
|
||||
if (vysledek.Count >= MaxCar) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Úseky přímky (parametr <c>t</c> od bodu <c>p</c> ve směru <c>d</c>), které leží
|
||||
/// uvnitř oblasti. Sudo-lichá parita jako u výplně: mezi 1. a 2. průsečíkem je uvnitř,
|
||||
/// mezi 2. a 3. venku – díky tomu vyjdou i díry ve vnořených smyčkách.</summary>
|
||||
private static List<(double Od, double Do)> Uvnitr(
|
||||
List<List<(double X, double Y)>> polygony, double px, double py, double dx, double dy)
|
||||
{
|
||||
var pruseciky = new List<double>();
|
||||
foreach (var polygon in polygony)
|
||||
for (int i = 0; i < polygon.Count; i++)
|
||||
{
|
||||
var a = polygon[i];
|
||||
var b = polygon[(i + 1) % polygon.Count];
|
||||
// Znaménko kolmé vzdálenosti vrcholu od přímky.
|
||||
double sa = (a.X - px) * -dy + (a.Y - py) * dx;
|
||||
double sb = (b.X - px) * -dy + (b.Y - py) * dx;
|
||||
if (sa == sb) continue; // hrana rovnoběžná s přímkou
|
||||
double podil = sa / (sa - sb);
|
||||
// Polouzavřený interval: vrchol sdílený dvěma hranami se započítá jen jednou,
|
||||
// jinak by parita na něm přeskočila a zbytek řádku vyšel obráceně.
|
||||
if (podil <= 0 || podil > 1) continue;
|
||||
double x = a.X + (b.X - a.X) * podil, y = a.Y + (b.Y - a.Y) * podil;
|
||||
pruseciky.Add((x - px) * dx + (y - py) * dy);
|
||||
}
|
||||
|
||||
pruseciky.Sort();
|
||||
var useky = new List<(double, double)>();
|
||||
for (int i = 0; i + 1 < pruseciky.Count; i += 2)
|
||||
if (pruseciky[i + 1] - pruseciky[i] > 1e-9)
|
||||
useky.Add((pruseciky[i], pruseciky[i + 1]));
|
||||
return useky;
|
||||
}
|
||||
|
||||
/// <summary>Čárkování jedné periody: intervaly, které se kreslí. <c>null</c> = plná čára.
|
||||
/// Nula v definici znamená tečku – kreslí se jako nepatrný úsek.</summary>
|
||||
private static (double Perioda, List<(double Od, double Do)> Kresli)? Carky(IEnumerable<double>? dashe)
|
||||
{
|
||||
var delky = dashe?.ToList();
|
||||
if (delky is not { Count: > 0 }) return null;
|
||||
|
||||
double perioda = delky.Sum(Math.Abs);
|
||||
if (perioda <= 1e-9) return null;
|
||||
|
||||
var kresli = new List<(double, double)>();
|
||||
double t = 0;
|
||||
foreach (var d in delky)
|
||||
{
|
||||
double l = Math.Abs(d) < 1e-9 ? 1e-6 : Math.Abs(d);
|
||||
if (d >= 0) kresli.Add((t, t + l));
|
||||
t += l;
|
||||
}
|
||||
return kresli.Count == 0 ? null : (perioda, kresli);
|
||||
}
|
||||
|
||||
/// <summary>Ořeže úsek přímky čárkováním. Bez čárkování projde beze změny.</summary>
|
||||
private static IEnumerable<(double Od, double Do)> Orez(
|
||||
(double Perioda, List<(double Od, double Do)> Kresli)? carky, double t1, double t2)
|
||||
{
|
||||
if (carky is not { } c) { yield return (t1, t2); yield break; }
|
||||
|
||||
long prvni = (long)Math.Floor(t1 / c.Perioda);
|
||||
long posledni = (long)Math.Ceiling(t2 / c.Perioda);
|
||||
if (posledni - prvni > MaxCar) { yield return (t1, t2); yield break; } // nesmyslně krátká perioda
|
||||
|
||||
for (long i = prvni; i <= posledni; i++)
|
||||
foreach (var (od, doT) in c.Kresli)
|
||||
{
|
||||
double a = Math.Max(t1, i * c.Perioda + od);
|
||||
double b = Math.Min(t2, i * c.Perioda + doT);
|
||||
if (b - a > 1e-9) yield return (a, b);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<List<(double X, double Y)>> Zplosti(IReadOnlyList<Contour> smycky)
|
||||
{
|
||||
var vysledek = new List<List<(double X, double Y)>>();
|
||||
foreach (var smycka in smycky)
|
||||
{
|
||||
var body = new List<(double X, double Y)>();
|
||||
foreach (var hrana in smycka.Edges) hrana.AppendPoints(body, Sagitta);
|
||||
if (body.Count >= 3) vysledek.Add(body);
|
||||
}
|
||||
return vysledek;
|
||||
}
|
||||
|
||||
private static (double MinX, double MinY, double MaxX, double MaxY) Rozsah(
|
||||
List<List<(double X, double Y)>> polygony)
|
||||
{
|
||||
double minX = double.MaxValue, minY = double.MaxValue, maxX = double.MinValue, maxY = double.MinValue;
|
||||
foreach (var polygon in polygony)
|
||||
foreach (var (x, y) in polygon)
|
||||
{
|
||||
if (x < minX) minX = x;
|
||||
if (y < minY) minY = y;
|
||||
if (x > maxX) maxX = x;
|
||||
if (y > maxY) maxY = y;
|
||||
}
|
||||
return (minX, minY, maxX, maxY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
namespace Rozpisky.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Paleta indexovaných barev AutoCADu (ACI 0–255). Index je to, co nese DXF group 62; na RGB se
|
||||
/// překládá až při rasterizaci (náhled, PNG, PDF) – do DXF se zapisuje zpátky jako <b>číslo</b>,
|
||||
/// takže se cestou tam a zpět nic neztratí.
|
||||
/// <para>Rozsah 10–249 je dopočítaný, ne vypsaný: 24 odstínů po 15° × 5 jasů, každý ve variantě
|
||||
/// sytá / poloviční sytost. Krajní hodnoty (0–9, 250–255) jsou tabulkou, protože do schématu
|
||||
/// nezapadají.</para>
|
||||
/// <para><b>Proč ne <c>ACadSharp.Color.GetIndexRGB</c>:</b> ta se od AutoCADu liší u indexů
|
||||
/// 8 a 9 (o dva stupně šedi), u 250–254 a u posledního indexu každé skupiny odstínů (19, 29, …,
|
||||
/// 249) – tam vrací hodnotu ze sousední skupiny, takže např. 29 a 39 dostanou stejnou barvu.
|
||||
/// Index 8 je přitom v <c>Rozpiska.dxf</c> použitý. Viz <c>AciPaletaTesty</c>.</para>
|
||||
/// </summary>
|
||||
public static class AciPaleta
|
||||
{
|
||||
/// <summary>ACI 0–9. Index 0 (ByBlock) a 7 se kreslí podle pozadí – tady černá / bílá.</summary>
|
||||
private static readonly int[] Zakladni =
|
||||
{
|
||||
0x000000, // 0 – ByBlock, samostatně se nekreslí
|
||||
0xFF0000, 0xFFFF00, 0x00FF00, 0x00FFFF, 0x0000FF, 0xFF00FF, 0xFFFFFF,
|
||||
0x414141, // 8 – tmavá šedá (ACadSharp tu má 0x808080)
|
||||
0x808080, // 9 – šedá (ACadSharp tu má 0xC0C0C0)
|
||||
};
|
||||
|
||||
/// <summary>ACI 250–255 – šedý klín zakončený bílou.</summary>
|
||||
private static readonly int[] Sede = { 0x333333, 0x505050, 0x696969, 0x828282, 0xBEBEBE, 0xFFFFFF };
|
||||
|
||||
/// <summary>Jas pro pozici v desetici: pětice 100 / 65 / 50 / 30 / 15 %, každá dvakrát
|
||||
/// (sudá pozice = plná sytost, lichá = poloviční).</summary>
|
||||
private static readonly int[] Jasy = { 0xFF, 0xFF, 0xA5, 0xA5, 0x7F, 0x7F, 0x4C, 0x4C, 0x26, 0x26 };
|
||||
|
||||
/// <summary>RGB 0xRRGGBB pro daný ACI index. Mimo rozsah 0–255 → černá.</summary>
|
||||
public static int Rgb(int index)
|
||||
{
|
||||
if (index is < 0 or > 255) return 0x000000;
|
||||
if (index <= 9) return Zakladni[index];
|
||||
if (index >= 250) return Sede[index - 250];
|
||||
return Odstin(index);
|
||||
}
|
||||
|
||||
/// <summary>Nejbližší ACI index k RGB (pro cizí soubory zapsané v true color). Prohledává
|
||||
/// 1–255 podle eukleidovské vzdálenosti v RGB – hrubé, ale pro převod stačí.</summary>
|
||||
public static byte NejblizsiIndex(int rgb)
|
||||
{
|
||||
int r = (rgb >> 16) & 0xFF, g = (rgb >> 8) & 0xFF, b = rgb & 0xFF;
|
||||
byte nej = 7;
|
||||
long nejVzdalenost = long.MaxValue;
|
||||
for (int i = 1; i <= 255; i++)
|
||||
{
|
||||
int c = Rgb(i);
|
||||
long dr = r - ((c >> 16) & 0xFF), dg = g - ((c >> 8) & 0xFF), db = b - (c & 0xFF);
|
||||
long d = dr * dr + dg * dg + db * db;
|
||||
if (d >= nejVzdalenost) continue;
|
||||
nejVzdalenost = d;
|
||||
nej = (byte)i;
|
||||
if (d == 0) break;
|
||||
}
|
||||
return nej;
|
||||
}
|
||||
|
||||
/// <summary>Barva odstínové části palety (10–249): odstín podle desítky, jas a sytost
|
||||
/// podle pozice v ní. Lichá pozice = tatáž barva smíchaná do poloviny s jasem
|
||||
/// (odbarvená), proto <c>c + (jas − c) / 2</c>.</summary>
|
||||
private static int Odstin(int index)
|
||||
{
|
||||
int skupina = (index - 10) / 10; // 0–23 → úhel odstínu 0°, 15°, … 345°
|
||||
int pozice = (index - 10) % 10; // 0–9 → jas a sytost
|
||||
int jas = Jasy[pozice];
|
||||
var (r, g, b) = Hsv(skupina * 15.0, jas);
|
||||
if ((pozice & 1) == 1)
|
||||
{
|
||||
r += (jas - r) / 2;
|
||||
g += (jas - g) / 2;
|
||||
b += (jas - b) / 2;
|
||||
}
|
||||
return (r << 16) | (g << 8) | b;
|
||||
}
|
||||
|
||||
/// <summary>Plně sytá barva daného odstínu a jasu (HSV se saturací 1). Dělení na šestiny
|
||||
/// kruhu; v každé roste jeden kanál a klesá jiný. Ořezává se dolů, jako to dělá AutoCAD.</summary>
|
||||
private static (int R, int G, int B) Hsv(double uhelDeg, int jas)
|
||||
{
|
||||
int sestina = (int)(uhelDeg / 60);
|
||||
double podil = uhelDeg / 60 - sestina;
|
||||
int rostouci = (int)(jas * podil);
|
||||
int klesajici = (int)(jas * (1 - podil));
|
||||
return sestina switch
|
||||
{
|
||||
0 => (jas, rostouci, 0),
|
||||
1 => (klesajici, jas, 0),
|
||||
2 => (0, jas, rostouci),
|
||||
3 => (0, klesajici, jas),
|
||||
4 => (rostouci, 0, jas),
|
||||
_ => (jas, 0, klesajici),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ public sealed class BoundsRenderer : IProfileRenderer
|
||||
if (y > MaxY) MaxY = y;
|
||||
}
|
||||
|
||||
public void Layer(string name, int rgb, double lineWeightMm = 0.25) { }
|
||||
public void Stav(in KresliciStav s) { }
|
||||
|
||||
public void Line(double x1, double y1, double x2, double y2) { Add(x1, y1); Add(x2, y2); }
|
||||
|
||||
@@ -55,7 +55,7 @@ public sealed class BoundsRenderer : IProfileRenderer
|
||||
Add(x + dx + w, y + dy + blockH);
|
||||
}
|
||||
|
||||
public void Fill(IReadOnlyList<Contour> loops)
|
||||
public void Fill(IReadOnlyList<Contour> loops, Vypln vypln = default)
|
||||
{
|
||||
foreach (var loop in loops) AddContour(loop);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Rozpisky.Rendering;
|
||||
/// (6 primitiv), takže má stejné pokrytí jako vykreslovací engine: složitější DXF entity (spline,
|
||||
/// oblouk, hatch) jsou už ve fázi čtení (<c>DxfTemplate.Replay</c>) převedené na čáry/polylinie/výplně.
|
||||
/// <para>Zachovává <b>barvu, tloušťku čáry a styl/zarovnání písma</b> přesně podle zdroje – stav se
|
||||
/// přebírá z <see cref="Layer"/> (barva + tloušťka pro každou entitu) a z parametru <c>style</c>
|
||||
/// přebírá z <see cref="Stav"/> (barva i s původem: ACI index / true color / ByLayer) a z parametru <c>style</c>
|
||||
/// metody <see cref="Text"/> (font). Souřadnice jsou ve výkresových mm, osa Y nahoru – shodně s DXF.
|
||||
/// Jednořádkový text → <c>TEXT</c>, víceřádkový (<c>maxWidthMm > 0</c>) → <c>MTEXT</c>.</para>
|
||||
/// </summary>
|
||||
@@ -36,27 +36,46 @@ public sealed class DxfProfileRenderer : IProfileRenderer
|
||||
_doc.Layers.Add(_aktualniVrstva);
|
||||
}
|
||||
_vrstvy[_aktualniVrstva.Name] = _aktualniVrstva;
|
||||
_aktualniBarva = new Color(0, 0, 0);
|
||||
_aktualniBarva = Color.ByLayer;
|
||||
_doc.Header.Version = VerzeVystupu;
|
||||
}
|
||||
|
||||
/// <summary>Hotový výkres se všemi rozpiskami (pro <c>DxfWriter.Write</c>).</summary>
|
||||
public CadDocument Document => _doc;
|
||||
|
||||
public void Layer(string name, int rgb, double lineWeightMm = 0.25)
|
||||
/// <summary>Verze DXF, ve které se výkres zapisuje – shodná se zdrojovou šablonou
|
||||
/// (AutoCAD 2013). Nastavuje se natvrdo: kdyby se nechala na výchozí hodnotě ACadSharp,
|
||||
/// změnil by ji tiše upgrade balíčku a s ní i to, čím půjde výkres otevřít.</summary>
|
||||
public const ACadVersion VerzeVystupu = ACadVersion.AC1027;
|
||||
|
||||
public void Stav(in KresliciStav s)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name)) name = AcLayer.DefaultName;
|
||||
if (!_vrstvy.TryGetValue(name, out var vrstva))
|
||||
_aktualniVrstva = NajdiVrstvu(s.Hladina);
|
||||
_aktualniBarva = NaColor(s.Barva);
|
||||
_aktualniTloustka = s.TloustkaMm is { } mm ? SnapTloustka(mm) : LineWeightType.ByLayer;
|
||||
}
|
||||
|
||||
/// <summary>Najde/založí hladinu podle popisu ze zdroje. Vlastnosti se berou z tabulky hladin,
|
||||
/// ne z první entity, která na hladině přistála – jinak by se ztratil příznak tisku
|
||||
/// (<c>!!NOPRINT</c>) a barva hladiny by byla náhodná.</summary>
|
||||
private AcLayer NajdiVrstvu(HladinaStav h)
|
||||
{
|
||||
string name = string.IsNullOrEmpty(h.Nazev) ? AcLayer.DefaultName : h.Nazev;
|
||||
if (_vrstvy.TryGetValue(name, out var vrstva)) return vrstva;
|
||||
|
||||
if (!_doc.Layers.TryGetValue(name, out vrstva!))
|
||||
{
|
||||
if (!_doc.Layers.TryGetValue(name, out vrstva!))
|
||||
{
|
||||
vrstva = new AcLayer(name) { Color = RgbNaColor(rgb) };
|
||||
_doc.Layers.Add(vrstva);
|
||||
}
|
||||
_vrstvy[name] = vrstva;
|
||||
vrstva = new AcLayer(name);
|
||||
_doc.Layers.Add(vrstva);
|
||||
}
|
||||
_aktualniVrstva = vrstva;
|
||||
_aktualniBarva = RgbNaColor(rgb); // barva entity dle zdroje (ByLayer už rozbalil DxfTemplate)
|
||||
_aktualniTloustka = SnapTloustka(lineWeightMm);
|
||||
vrstva.Color = h.Barva.JeVlastni ? NaColor(h.Barva) : new Color(7);
|
||||
vrstva.LineWeight = h.TloustkaMm > 0 ? SnapTloustka(h.TloustkaMm) : LineWeightType.Default;
|
||||
vrstva.PlotFlag = h.Tisknout;
|
||||
vrstva.IsOn = h.Zapnuta;
|
||||
if (!string.IsNullOrWhiteSpace(h.TypCary) && _doc.LineTypes.TryGetValue(h.TypCary, out var lt))
|
||||
vrstva.LineType = lt;
|
||||
_vrstvy[name] = vrstva;
|
||||
return vrstva;
|
||||
}
|
||||
|
||||
public void Line(double x1, double y1, double x2, double y2)
|
||||
@@ -111,9 +130,11 @@ public sealed class DxfProfileRenderer : IProfileRenderer
|
||||
{
|
||||
var mtext = new MText
|
||||
{
|
||||
Value = text.Replace("\r\n", "\\P").Replace("\n", "\\P"),
|
||||
Value = MTextHodnota(text, widthFactor),
|
||||
InsertPoint = new XYZ(x, y, 0),
|
||||
Height = heightMm,
|
||||
// MTEXT nemá zapisovatelné natočení – nese ho směrový vektor osy X (group 11).
|
||||
AlignmentPoint = SmerOsyX(rotationDeg),
|
||||
RectangleWidth = maxWidthMm,
|
||||
AttachmentPoint = MapAttach(h, v),
|
||||
};
|
||||
@@ -139,12 +160,26 @@ public sealed class DxfProfileRenderer : IProfileRenderer
|
||||
}
|
||||
}
|
||||
|
||||
public void Fill(IReadOnlyList<Contour> loops)
|
||||
public void Fill(IReadOnlyList<Contour> loops, Vypln vypln = default)
|
||||
{
|
||||
if (loops.Count == 0) return;
|
||||
try
|
||||
{
|
||||
var hatch = new Hatch { IsSolid = true, Pattern = new HatchPattern("SOLID") };
|
||||
// Vzor šrafy se zapisuje zpátky i s definičními čarami – DXF je nese uvnitř entity
|
||||
// HATCH, a bez nich si cílový CAD vzor dosadí ze svých .pat, ve svých jednotkách
|
||||
// a měřítku. Rozvinuté čáry (Vypln.Cary) jsou naopak jen pro rastrové renderery.
|
||||
//
|
||||
// Měřítko a úhel zůstávají neutrální (1 a 0), protože definiční čáry jsou už
|
||||
// v efektivních hodnotách – měřítko i natočení jsou v nich započítané. ACadSharp
|
||||
// při zápisu násobí rozteč vzoru hodnotou PatternScale, ale při čtení zpátky nedělí;
|
||||
// s nenulovým měřítkem by se tak vzor s každým kolečkem roztáhl.
|
||||
var hatch = new Hatch
|
||||
{
|
||||
IsSolid = vypln.JeSolid,
|
||||
Pattern = VzorProHatch(vypln),
|
||||
PatternScale = 1,
|
||||
PatternAngle = 0,
|
||||
};
|
||||
foreach (var loop in loops)
|
||||
{
|
||||
if (loop.Edges.Count == 0) continue;
|
||||
@@ -172,6 +207,62 @@ public sealed class DxfProfileRenderer : IProfileRenderer
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Vzor pro entitu HATCH: jméno a definiční čáry. Bez definičních čar má výstup jen
|
||||
/// jméno vzoru a cílový CAD si podle něj dosadí vlastní definici – v jiných jednotkách
|
||||
/// a jiném měřítku, takže šrafa vypadá v každém výkresu jinak.</summary>
|
||||
private static HatchPattern VzorProHatch(Vypln vypln)
|
||||
{
|
||||
var vzor = new HatchPattern(vypln.JeSolid ? "SOLID" : vypln.Vzor!);
|
||||
if (vypln.JeSolid) return vzor;
|
||||
|
||||
foreach (var cara in vypln.Definice ?? Array.Empty<VzorCara>())
|
||||
{
|
||||
var radek = new HatchPattern.Line
|
||||
{
|
||||
Angle = cara.UhelDeg * Math.PI / 180.0,
|
||||
BasePoint = new XY(cara.Zaklad.X, cara.Zaklad.Y),
|
||||
Offset = new XY(cara.Offset.X, cara.Offset.Y),
|
||||
};
|
||||
foreach (var d in cara.Carky) radek.DashLengths.Add(d);
|
||||
vzor.Lines.Add(radek);
|
||||
}
|
||||
return vzor;
|
||||
}
|
||||
|
||||
/// <summary>Směrový vektor osy X textu pro dané natočení – tím se natáčí MTEXT.</summary>
|
||||
private static XYZ SmerOsyX(double rotationDeg)
|
||||
{
|
||||
double rad = rotationDeg * Math.PI / 180.0;
|
||||
return new XYZ(Math.Cos(rad), Math.Sin(rad), 0);
|
||||
}
|
||||
|
||||
/// <summary>Text pro MTEXT. Escapuje znaky, které jsou v MTEXTu řídicí (zpětné lomítko a
|
||||
/// složené závorky) – bez toho by uživatelova hodnota se zpětným lomítkem rozbila formátování –,
|
||||
/// zlomy řádků převede na <c>\P</c> a šířkový faktor doplní kódem <c>\W</c>: MTEXT ho na rozdíl
|
||||
/// od TEXTu nemá jako vlastnost entity, takže jinak by se ztratil.</summary>
|
||||
private static string MTextHodnota(string text, double widthFactor)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
if (widthFactor > 0 && Math.Abs(widthFactor - 1) > 1e-9)
|
||||
sb.Append(@"\W")
|
||||
.Append(widthFactor.ToString("0.####", System.Globalization.CultureInfo.InvariantCulture))
|
||||
.Append(';');
|
||||
|
||||
foreach (var znak in text)
|
||||
{
|
||||
switch (znak)
|
||||
{
|
||||
case '\\': sb.Append(@"\\"); break;
|
||||
case '{': sb.Append(@"\{"); break;
|
||||
case '}': sb.Append(@"\}"); break;
|
||||
case '\n': sb.Append(@"\P"); break;
|
||||
case '\r': break;
|
||||
default: sb.Append(znak); break;
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>Převede analytickou hranu obrysu na DXF hraniční edge šrafy (úsečka / kruhový oblouk).
|
||||
/// U oblouku: záporný rozsah (CW) se v DXF vyjádří <c>CounterClockWise=false</c>, kde čtečka úhly
|
||||
/// zrcadlí přes osu X – proto pro CW meze <c>−A0</c> / <c>−(A0+Sweep)</c>, znormalizované do [0,2π).</summary>
|
||||
@@ -214,20 +305,41 @@ public sealed class DxfProfileRenderer : IProfileRenderer
|
||||
/// stylu (použije se výchozí styl dokumentu).</summary>
|
||||
private AcTextStyle? StylProEntitu(TextStyle s)
|
||||
{
|
||||
// Soubor fontu: přednostně .ttf z předlohy, jinak odvozeno z rodiny písma.
|
||||
// Soubor fontu: přednostně ten z předlohy (.ttf i .shx), jinak odvozeno z rodiny písma.
|
||||
string? file = s.FontFile ?? (string.IsNullOrWhiteSpace(s.FontFamily) ? null : s.FontFamily + ".ttf");
|
||||
if (file is null && string.IsNullOrWhiteSpace(s.FontFamily)) return null;
|
||||
if (file is null && string.IsNullOrWhiteSpace(s.FontFamily) && string.IsNullOrWhiteSpace(s.Nazev))
|
||||
return null;
|
||||
|
||||
string klic = $"{file}|{s.FontFamily}|{(s.Bold ? "B" : "")}{(s.Italic ? "I" : "")}|{s.ObliqueDeg}";
|
||||
// Klíčem je jméno stylu ze zdroje, pokud ho zná – jeden zdrojový styl = jeden styl ve
|
||||
// výstupu. Bez toho vznikal styl pro každou kombinaci fontu a sklonu a jména se
|
||||
// vymýšlela (Verdana_1, Arial_2), takže se ve správci stylů nedalo nic najít.
|
||||
string klic = string.IsNullOrWhiteSpace(s.Nazev)
|
||||
? $"\0odvozeny|{file}|{s.FontFamily}|{(s.Bold ? "B" : "")}{(s.Italic ? "I" : "")}|{s.ObliqueDeg}"
|
||||
: s.Nazev!;
|
||||
if (_styly.TryGetValue(klic, out var styl)) return styl;
|
||||
|
||||
var ff = FontFlags.Regular;
|
||||
if (s.Bold) ff |= FontFlags.Bold;
|
||||
if (s.Italic) ff |= FontFlags.Italic;
|
||||
|
||||
string nazev = UnikatniNazevStylu(s.FontFamily ?? Path.GetFileNameWithoutExtension(file) ?? "Styl", s.Bold, s.Italic);
|
||||
string nazev = string.IsNullOrWhiteSpace(s.Nazev)
|
||||
? UnikatniNazevStylu(s.FontFamily ?? Path.GetFileNameWithoutExtension(file) ?? "Styl", s.Bold, s.Italic)
|
||||
: s.Nazev!;
|
||||
|
||||
if (_doc.TextStyles.TryGetValue(nazev, out var existujici))
|
||||
{
|
||||
_styly[klic] = existujici;
|
||||
return existujici;
|
||||
}
|
||||
|
||||
// Oblique i na stylu – TEXT ho nese sám (group 51), ale MTEXT sklon přebírá jen ze stylu.
|
||||
styl = new AcTextStyle(nazev) { Filename = file ?? "", TrueType = ff, ObliqueAngle = s.ObliqueDeg * Math.PI / 180.0 };
|
||||
styl = new AcTextStyle(nazev)
|
||||
{
|
||||
Filename = file ?? "",
|
||||
TrueType = ff,
|
||||
ObliqueAngle = s.ObliqueDeg * Math.PI / 180.0,
|
||||
Flags = s.JeShx ? ACadSharp.Tables.StyleFlags.IsShape : ACadSharp.Tables.StyleFlags.None,
|
||||
};
|
||||
_doc.TextStyles.Add(styl);
|
||||
_styly[klic] = styl;
|
||||
return styl;
|
||||
@@ -246,8 +358,16 @@ public sealed class DxfProfileRenderer : IProfileRenderer
|
||||
return kandidat;
|
||||
}
|
||||
|
||||
private static Color RgbNaColor(int rgb)
|
||||
=> new((byte)((rgb >> 16) & 0xFF), (byte)((rgb >> 8) & 0xFF), (byte)(rgb & 0xFF));
|
||||
/// <summary>Barva do DXF <b>ve stejném druhu, v jakém přišla ze zdroje</b>: indexovaná zůstane
|
||||
/// číslem (group 62), přímá se zapíše jako RGB (group 420) a ByLayer/ByBlock zůstane odkazem.
|
||||
/// Dřív se všechno zplošťovalo na RGB, takže výkres v CADu přestal reagovat na barvy hladin.</summary>
|
||||
private static Color NaColor(Barva b) => b.Druh switch
|
||||
{
|
||||
BarvaDruh.Index => new Color((short)b.Index),
|
||||
BarvaDruh.True => new Color((byte)((b.Rgb >> 16) & 0xFF), (byte)((b.Rgb >> 8) & 0xFF), (byte)(b.Rgb & 0xFF)),
|
||||
BarvaDruh.ByBlock => Color.ByBlock,
|
||||
_ => Color.ByLayer,
|
||||
};
|
||||
|
||||
/// <summary>Standardní tloušťky čar (1/100 mm) dle DXF; vstupní mm se přichytí k nejbližší.</summary>
|
||||
private static readonly int[] StdTloustky =
|
||||
|
||||
@@ -8,12 +8,18 @@ public enum VAlign { Baseline, Bottom, Middle, Top }
|
||||
|
||||
/// <summary>
|
||||
/// Vzhled písma přenesený z DXF textového stylu (ROZPISKY.md §3): rodina fontu + řez,
|
||||
/// volitelně přímo soubor fontu (.ttf) pro věrný řez. <c>default</c> = fallback font rendereru.
|
||||
/// volitelně přímo soubor fontu pro věrný řez. <c>default</c> = fallback font rendereru.
|
||||
/// <paramref name="ObliqueDeg"/> = sklon znaků ve stupních (DXF oblique angle, group 51 entity /
|
||||
/// group 50 stylu); kladný = naklonění doprava, 0 = vzpřímeně.
|
||||
/// <para><paramref name="Nazev"/> = jméno stylu ve zdrojovém výkresu („Popis základní",
|
||||
/// „Název stavby"). Nese se dál, aby ho DXF export zapsal beze změny – dřív se zahazovalo
|
||||
/// a v exportu vznikala náhradní jména typu <c>Verdana_1</c>. <paramref name="JeShx"/> = styl
|
||||
/// odkazuje na vektorový font AutoCADu (.shx); ten se nedá načíst jako TrueType, takže se
|
||||
/// kreslí náhradním fontem, ale do DXF se zapíše původní.</para>
|
||||
/// </summary>
|
||||
public readonly record struct TextStyle(string? FontFamily = null, bool Bold = false, bool Italic = false,
|
||||
string? FontFile = null, double ObliqueDeg = 0);
|
||||
string? FontFile = null, double ObliqueDeg = 0,
|
||||
string? Nazev = null, bool JeShx = false);
|
||||
|
||||
/// <summary>Druh hrany obrysu: úsečka nebo kruhový oblouk. Elipsy a spliny se do obrysu vkládají
|
||||
/// jako jemná řada úseček (viz <see cref="Edge"/>), takže renderery vystačí se dvěma případy.</summary>
|
||||
@@ -47,6 +53,42 @@ public readonly record struct Edge(
|
||||
public (double X, double Y) PointAt(double theta) => (Cx + R * Math.Cos(theta), Cy + R * Math.Sin(theta));
|
||||
}
|
||||
|
||||
/// <summary>Úsečka ve výkresových mm – čára vzoru šrafy.</summary>
|
||||
public readonly record struct Usecka(double X1, double Y1, double X2, double Y2);
|
||||
|
||||
/// <summary>
|
||||
/// Jedna definiční čára vzoru šrafy = celá rodina rovnoběžek: směr <paramref name="UhelDeg"/>,
|
||||
/// jedna čára prochází <paramref name="Zaklad"/>, sousední jsou posunuté o vektor
|
||||
/// <paramref name="Offset"/> (kolmá složka = rozteč, podélná posouvá čárkování).
|
||||
/// <paramref name="Carky"/> = délky čárek a mezer (záporná = mezera); prázdné = plná čára.
|
||||
/// <para>Tohle je <b>skutečná definice vzoru</b>, ne jen jeho jméno. DXF ji nese uvnitř entity
|
||||
/// <c>HATCH</c>, takže je nutné ji zapsat i do výstupu – jinak si ji cílový CAD dosadí ze svých
|
||||
/// <c>.pat</c> souborů, ve svých jednotkách a měřítku, a šrafa vyjde pokaždé jinak.</para>
|
||||
/// </summary>
|
||||
public readonly record struct VzorCara(
|
||||
double UhelDeg, (double X, double Y) Zaklad, (double X, double Y) Offset, IReadOnlyList<double> Carky);
|
||||
|
||||
/// <summary>
|
||||
/// Druh výplně plochy. <c>default</c> = plná výplň (SOLID).
|
||||
/// <para>Nesolidní šrafa (ANSI31 apod.) se rastrovým rendererům předává už rozvinutá na
|
||||
/// <paramref name="Cary"/>, protože Skia vzory šraf neumí; DXF export naopak <paramref name="Cary"/>
|
||||
/// ignoruje a zapíše zpátky <c>HATCH</c> se jménem vzoru, měřítkem, úhlem a <b>definičními čarami</b>
|
||||
/// (<paramref name="Definice"/>), aby zůstal editovatelný a vypadal stejně jako ve zdroji. Dřív se
|
||||
/// každá šrafa kreslila jako plná výplň – z logotypu <c>md_sfdi.dxf</c> byl černý blok.</para>
|
||||
/// </summary>
|
||||
public readonly record struct Vypln(
|
||||
string? Vzor = null, double Meritko = 1, double UhelDeg = 0,
|
||||
IReadOnlyList<Usecka>? Cary = null, IReadOnlyList<VzorCara>? Definice = null)
|
||||
{
|
||||
/// <summary>Plná výplň – buď se vzor neuvádí (<c>default</c>), nebo je to SOLID.</summary>
|
||||
public bool JeSolid => string.IsNullOrEmpty(Vzor) || Vzor.Equals("SOLID", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>Šrafa se vzorem: definice pro DXF a z ní rozvinuté čáry pro rastr.</summary>
|
||||
public static Vypln Vzorovana(string vzor, double meritko, double uhelDeg,
|
||||
IReadOnlyList<VzorCara> definice, IReadOnlyList<Usecka> cary)
|
||||
=> new(vzor, meritko, uhelDeg, cary, definice);
|
||||
}
|
||||
|
||||
/// <summary>Obrys = zřetězené hrany (konec jedné = počátek další). <paramref name="Closed"/> = true →
|
||||
/// vyplnitelná/uzavřená smyčka (poslední hrana se vrací k počátku prvního).</summary>
|
||||
public readonly record struct Contour(IReadOnlyList<Edge> Edges, bool Closed);
|
||||
@@ -84,10 +126,12 @@ public static class ContourGeometry
|
||||
/// </summary>
|
||||
public interface IProfileRenderer
|
||||
{
|
||||
/// <summary>Nastaví aktuální hladinu/styl kresby. <paramref name="rgb"/> = barva 0xRRGGBB
|
||||
/// (rozbalená z DXF, ByLayer i index/true-color), tloušťka v mm. Bílá se kreslí černě
|
||||
/// <summary>Nastaví aktuální stav kresby – hladinu a to, čím ji entita přebíjí
|
||||
/// (<see cref="KresliciStav"/>). Barva si nese svůj původ (ACI index / true color / ByLayer),
|
||||
/// aby ji DXF export mohl zapsat tak, jak byla ve zdroji. Rastrové renderery si vyžádají
|
||||
/// hotové RGB přes <see cref="KresliciStav.RgbProKresbu"/>; bílá se kreslí černě
|
||||
/// (bílá na bílém papíře by zmizela).</summary>
|
||||
void Layer(string name, int rgb, double lineWeightMm = 0.25);
|
||||
void Stav(in KresliciStav s);
|
||||
void Line(double x1, double y1, double x2, double y2);
|
||||
void Polyline(IReadOnlyList<(double X, double Y)> pts, bool closed = false);
|
||||
void Circle(double cx, double cy, double r);
|
||||
@@ -103,6 +147,7 @@ public interface IProfileRenderer
|
||||
|
||||
/// <summary>Vyplní oblast danou jednou či více uzavřenými obrysy (even-odd → vnořené smyčky
|
||||
/// tvoří díry). Obrysy nesou analytické oblouky (viz <see cref="Contour"/>), takže výplně z
|
||||
/// DXF HATCH – např. loga – mají hladké okraje v každém přiblížení i v PDF.</summary>
|
||||
void Fill(IReadOnlyList<Contour> loops);
|
||||
/// DXF HATCH – např. loga – mají hladké okraje v každém přiblížení i v PDF.
|
||||
/// <paramref name="vypln"/> říká, čím se plocha vyplní; <c>default</c> = plná.</summary>
|
||||
void Fill(IReadOnlyList<Contour> loops, Vypln vypln = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
namespace Rozpisky.Rendering;
|
||||
|
||||
/// <summary>Původ barvy podle DXF. <see cref="ByLayer"/> a <see cref="ByBlock"/> nenesou vlastní
|
||||
/// hodnotu – barva se dopočítá z hladiny (resp. z vkládaného bloku).</summary>
|
||||
public enum BarvaDruh : byte { ByLayer, ByBlock, Index, True }
|
||||
|
||||
/// <summary>
|
||||
/// Barva tak, jak je zapsaná v DXF – <b>i s původem</b>, ne jen výsledné RGB. Bez toho by se
|
||||
/// při exportu z indexované barvy (group 62, „barva 5") stala natvrdo RGB (group 420) a výkres
|
||||
/// by v CADu přestal reagovat na tabulku barev; ByLayer by se rozpadlo na kopie barvy hladiny.
|
||||
/// <para><paramref name="Rgb"/> je vždy vyplněné pro kresbu na obrazovku; u ByLayer/ByBlock je
|
||||
/// bezvýznamné (rozřeší se až v <see cref="KresliciStav.RgbProKresbu"/>).</para>
|
||||
/// </summary>
|
||||
public readonly record struct Barva(BarvaDruh Druh, byte Index, int Rgb)
|
||||
{
|
||||
/// <summary>Indexovaná barva AutoCADu (ACI 1–255).</summary>
|
||||
public static Barva Aci(int index) => new(BarvaDruh.Index, (byte)index, AciPaleta.Rgb(index));
|
||||
|
||||
/// <summary>Přímá barva (true color, DXF group 420).</summary>
|
||||
public static Barva Prima(int rgb) => new(BarvaDruh.True, 0, rgb & 0xFFFFFF);
|
||||
|
||||
/// <summary>Barva se převezme z hladiny (ACI 256).</summary>
|
||||
public static readonly Barva ZHladiny = new(BarvaDruh.ByLayer, 0, 0);
|
||||
|
||||
/// <summary>Barva se převezme z bloku, do kterého je entita vložená (ACI 0).</summary>
|
||||
public static readonly Barva ZBloku = new(BarvaDruh.ByBlock, 0, 0);
|
||||
|
||||
/// <summary>true = barva nese vlastní hodnotu a nemusí se dohledávat jinde.</summary>
|
||||
public bool JeVlastni => Druh is BarvaDruh.Index or BarvaDruh.True;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Vlastnosti hladiny ze zdrojového výkresu. Přenášejí se celé, aby se hladina dala ve výstupu
|
||||
/// postavit znovu – dřív se její barva odvozovala od první entity, která na ní přistála, a
|
||||
/// příznak tisku se ztrácel (rámeček <c>!!NOPRINT</c> pak ve výstupu vycházel tisknutelný).
|
||||
/// </summary>
|
||||
public readonly record struct HladinaStav(
|
||||
string Nazev, Barva Barva, string TypCary = "Continuous",
|
||||
double TloustkaMm = 0, bool Tisknout = true, bool Zapnuta = true)
|
||||
{
|
||||
/// <summary>Hladina „0" s barvou 7 – použije se, když zdroj hladinu neuvádí.</summary>
|
||||
public static readonly HladinaStav Vychozi = new("0", Barva.Aci(7));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stav kresby pro jednu entitu: hladina a to, čím ji entita přebíjí. <c>null</c> u typu čáry
|
||||
/// i tloušťky znamená „podle hladiny" – stejná sémantika jako v DXF.
|
||||
/// </summary>
|
||||
public readonly record struct KresliciStav(
|
||||
HladinaStav Hladina, Barva Barva, string? TypCary = null, double? TloustkaMm = null)
|
||||
{
|
||||
/// <summary>Výchozí tloušťka, když ji neurčí ani entita, ani hladina (DXF „Default").</summary>
|
||||
public const double VychoziTloustkaMm = 0.18;
|
||||
|
||||
/// <summary>Barva pro rasterizaci: ByLayer i ByBlock se rozřeší barvou hladiny.
|
||||
/// (ByBlock se sem dostane jen mimo blok, kde se chová jako ByLayer.)</summary>
|
||||
public int RgbProKresbu()
|
||||
{
|
||||
if (Barva.JeVlastni) return Barva.Rgb;
|
||||
return Hladina.Barva.JeVlastni ? Hladina.Barva.Rgb : 0x000000;
|
||||
}
|
||||
|
||||
/// <summary>Tloušťka čáry v mm pro rasterizaci.</summary>
|
||||
public double TloustkaKresby()
|
||||
{
|
||||
double mm = TloustkaMm ?? Hladina.TloustkaMm;
|
||||
return mm > 0 ? mm : VychoziTloustkaMm;
|
||||
}
|
||||
|
||||
/// <summary>Název typu čáry (Continuous, DASHED…).</summary>
|
||||
public string TypCaryKresby()
|
||||
{
|
||||
var t = TypCary ?? Hladina.TypCary;
|
||||
return string.IsNullOrWhiteSpace(t) ? "Continuous" : t;
|
||||
}
|
||||
|
||||
/// <summary>Stav pro kresbu mimo zdrojový výkres (rámečky, pomocná grafika).</summary>
|
||||
public static KresliciStav Vychozi => new(HladinaStav.Vychozi, Barva.ZHladiny);
|
||||
}
|
||||
@@ -66,7 +66,8 @@ public sealed class SkiaProfileRenderer : IProfileRenderer, IDisposable
|
||||
private SKTypeface Typeface(TextStyle style)
|
||||
{
|
||||
// 1) Přesný font ze souboru, na který odkazuje DXF styl (.ttf v systémové složce Fonts).
|
||||
if (!string.IsNullOrWhiteSpace(style.FontFile))
|
||||
// SHX (vektorový font AutoCADu) se takhle načíst nedá – jde rovnou na náhradu.
|
||||
if (!style.JeShx && !string.IsNullOrWhiteSpace(style.FontFile))
|
||||
{
|
||||
var fileKey = ("\0file:" + style.FontFile, 0, false);
|
||||
if (_typefaces.TryGetValue(fileKey, out var tff)) return tff;
|
||||
@@ -116,9 +117,10 @@ public sealed class SkiaProfileRenderer : IProfileRenderer, IDisposable
|
||||
|
||||
private float StrokePx => Math.Max((float)(_lineWeightMm * _pxPerMm), _minStrokePx);
|
||||
|
||||
public void Layer(string name, int rgb, double lineWeightMm = 0.25)
|
||||
public void Stav(in KresliciStav s)
|
||||
{
|
||||
_lineWeightMm = lineWeightMm;
|
||||
_lineWeightMm = s.TloustkaKresby();
|
||||
int rgb = s.RgbProKresbu();
|
||||
byte r = (byte)((rgb >> 16) & 0xFF), g = (byte)((rgb >> 8) & 0xFF), b = (byte)(rgb & 0xFF);
|
||||
if (r == 255 && g == 255 && b == 255) r = g = b = 0; // bílá na bílém papíře → černá
|
||||
var c = new SKColor(r, g, b);
|
||||
@@ -259,9 +261,21 @@ public sealed class SkiaProfileRenderer : IProfileRenderer, IDisposable
|
||||
return lines;
|
||||
}
|
||||
|
||||
public void Fill(IReadOnlyList<Contour> loops)
|
||||
public void Fill(IReadOnlyList<Contour> loops, Vypln vypln = default)
|
||||
{
|
||||
if (loops.Count == 0) return;
|
||||
|
||||
// Šrafa se vzorem (ANSI31 apod.): Skia vzory neumí, kreslí se rozvinuté čáry. Když se
|
||||
// vzor rozvinout nepodařilo, zůstane plocha prázdná – zaplácnout ji načerno by bylo horší.
|
||||
if (!vypln.JeSolid)
|
||||
{
|
||||
if (vypln.Cary is not { Count: > 0 }) return;
|
||||
_stroke.StrokeWidth = StrokePx;
|
||||
foreach (var u in vypln.Cary)
|
||||
_canvas.DrawLine(_map(u.X1, u.Y1), _map(u.X2, u.Y2), _stroke);
|
||||
return;
|
||||
}
|
||||
|
||||
using var path = new SKPath { FillType = SKPathFillType.EvenOdd };
|
||||
bool any = false;
|
||||
foreach (var loop in loops)
|
||||
|
||||
@@ -1,53 +1,171 @@
|
||||
namespace Rozpisky.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Dekorátor <see cref="IProfileRenderer"/>, který na všechny souřadnice aplikuje
|
||||
/// uniformní měřítko + posun: <c>X' = Ox + S·X</c>, <c>Y' = Oy + S·Y</c>. Slouží k umístění
|
||||
/// externího DXF fragmentu (logo / orientační schéma) do vymezeného rámečku rozpisky –
|
||||
/// fragment se nakreslí ve svých souřadnicích a tato vrstva ho přemapuje do cílového obdélníku.
|
||||
/// Výška textu, poloměr a tloušťka čar se škálují <c>·S</c>; hladina/barva propadá beze změny.
|
||||
/// Dekorátor <see cref="IProfileRenderer"/>, který na souřadnice aplikuje afinní transformaci
|
||||
/// <c>X' = A·x + C·y + Tx</c>, <c>Y' = B·x + D·y + Ty</c>.
|
||||
/// <para>Dvě použití: umístění externího DXF fragmentu (logo / orientační schéma) do rámečku
|
||||
/// rozpisky – tam stačí měřítko a posun (<see cref="TransformRenderer(IProfileRenderer,double,double,double)"/>)
|
||||
/// – a rozvinutí vloženého bloku (<c>INSERT</c>), kde přibude natočení a nestejné měřítko os
|
||||
/// (<see cref="ProBlok"/>).</para>
|
||||
/// <para>Kruhový oblouk zůstane obloukem jen tehdy, je-li transformace <b>podobnost</b> (stejné
|
||||
/// měřítko obou os a kolmé osy). Při nestejném měřítku by z něj byla elipsa, kterou kreslicí
|
||||
/// rozhraní nezná – tam se oblouk rozloží na lomenou čáru.</para>
|
||||
/// </summary>
|
||||
public sealed class TransformRenderer : IProfileRenderer
|
||||
{
|
||||
private readonly IProfileRenderer _inner;
|
||||
private readonly double _s, _ox, _oy;
|
||||
private readonly double _a, _b, _c, _d, _tx, _ty;
|
||||
|
||||
/// <summary>Měřítko délek (u podobnosti přesné, jinak odmocnina z determinantu).</summary>
|
||||
private readonly double _meritko;
|
||||
|
||||
/// <summary>Natočení transformace ve stupních – přičítá se k natočení textů.</summary>
|
||||
private readonly double _uhelDeg;
|
||||
|
||||
/// <summary>true = oblouky a kružnice zůstanou oblouky a kružnicemi. Platí pro podobnost bez
|
||||
/// zrcadlení; při nestejném měřítku os by z kružnice byla elipsa a při zrcadlení by se obrátil
|
||||
/// smysl oblouku – v obou případech je bezpečnější rozložit tvar na lomenou čáru.</summary>
|
||||
private readonly bool _zachovavaOblouky;
|
||||
|
||||
/// <summary>Uniformní měřítko + posun, bez natočení (umístění fragmentu do rámečku).</summary>
|
||||
public TransformRenderer(IProfileRenderer inner, double scale, double offsetX, double offsetY)
|
||||
: this(inner, scale, 0, 0, scale, offsetX, offsetY) { }
|
||||
|
||||
private TransformRenderer(IProfileRenderer inner, double a, double b, double c, double d, double tx, double ty)
|
||||
{
|
||||
_inner = inner;
|
||||
_s = scale;
|
||||
_ox = offsetX;
|
||||
_oy = offsetY;
|
||||
_a = a; _b = b; _c = c; _d = d; _tx = tx; _ty = ty;
|
||||
|
||||
double delkaX = Math.Sqrt(a * a + b * b), delkaY = Math.Sqrt(c * c + d * d);
|
||||
double skalar = a * c + b * d; // 0 = osy jsou kolmé
|
||||
bool podobnost = Math.Abs(delkaX - delkaY) <= 1e-9 * Math.Max(delkaX, 1)
|
||||
&& Math.Abs(skalar) <= 1e-9 * Math.Max(delkaX * delkaY, 1);
|
||||
|
||||
double det = a * d - b * c;
|
||||
_zachovavaOblouky = podobnost && det > 0;
|
||||
_meritko = podobnost ? delkaX : Math.Sqrt(Math.Abs(det));
|
||||
_uhelDeg = Math.Atan2(b, a) * 180 / Math.PI;
|
||||
}
|
||||
|
||||
private double X(double x) => _ox + _s * x;
|
||||
private double Y(double y) => _oy + _s * y;
|
||||
/// <summary>Transformace vloženého bloku: entity bloku jsou zapsané vůči jeho vztažnému bodu,
|
||||
/// ten se klade na vkládací bod. Tedy <c>p' = vkládací + R·S·(p − vztažný)</c>. Nulové měřítko
|
||||
/// (setkáno v poškozených souborech) se bere jako 1, jinak by blok zkolaboval do bodu.</summary>
|
||||
public static TransformRenderer ProBlok(
|
||||
IProfileRenderer inner, double vlozX, double vlozY, double zakladX, double zakladY,
|
||||
double meritkoX, double meritkoY, double natoceniRad)
|
||||
{
|
||||
if (meritkoX == 0) meritkoX = 1;
|
||||
if (meritkoY == 0) meritkoY = 1;
|
||||
double cos = Math.Cos(natoceniRad), sin = Math.Sin(natoceniRad);
|
||||
double a = meritkoX * cos, b = meritkoX * sin, c = -meritkoY * sin, d = meritkoY * cos;
|
||||
return new TransformRenderer(
|
||||
inner, a, b, c, d,
|
||||
vlozX - (a * zakladX + c * zakladY),
|
||||
vlozY - (b * zakladX + d * zakladY));
|
||||
}
|
||||
|
||||
public void Layer(string name, int rgb, double lineWeightMm = 0.25)
|
||||
=> _inner.Layer(name, rgb, lineWeightMm * _s);
|
||||
private double X(double x, double y) => _tx + _a * x + _c * y;
|
||||
private double Y(double x, double y) => _ty + _b * x + _d * y;
|
||||
|
||||
public void Stav(in KresliciStav s)
|
||||
=> _inner.Stav(s with
|
||||
{
|
||||
TloustkaMm = s.TloustkaMm * _meritko,
|
||||
Hladina = s.Hladina with { TloustkaMm = s.Hladina.TloustkaMm * _meritko },
|
||||
});
|
||||
|
||||
public void Line(double x1, double y1, double x2, double y2)
|
||||
=> _inner.Line(X(x1), Y(y1), X(x2), Y(y2));
|
||||
=> _inner.Line(X(x1, y1), Y(x1, y1), X(x2, y2), Y(x2, y2));
|
||||
|
||||
public void Polyline(IReadOnlyList<(double X, double Y)> pts, bool closed = false)
|
||||
=> _inner.Polyline(pts.Select(p => (X(p.X), Y(p.Y))).ToList(), closed);
|
||||
=> _inner.Polyline(pts.Select(p => (X(p.X, p.Y), Y(p.X, p.Y))).ToList(), closed);
|
||||
|
||||
public void Circle(double cx, double cy, double r)
|
||||
=> _inner.Circle(X(cx), Y(cy), r * _s);
|
||||
{
|
||||
if (_zachovavaOblouky) { _inner.Circle(X(cx, cy), Y(cx, cy), r * _meritko); return; }
|
||||
|
||||
public void Stroke(Contour c) => _inner.Stroke(new Contour(c.Edges.Select(T).ToList(), c.Closed));
|
||||
// Nestejné měřítko os → z kružnice je elipsa; kreslicí rozhraní ji nezná, tedy lomená čára.
|
||||
int n = Math.Max(16, ContourGeometry.ArcSegments(2 * Math.PI, Math.Max(r * _meritko, 1e-9), 0.02));
|
||||
var body = new List<(double X, double Y)>(n);
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
double t = 2 * Math.PI * i / n;
|
||||
double x = cx + r * Math.Cos(t), y = cy + r * Math.Sin(t);
|
||||
body.Add((X(x, y), Y(x, y)));
|
||||
}
|
||||
_inner.Polyline(body, closed: true);
|
||||
}
|
||||
|
||||
/// <summary>Přemapuje hranu: body scale+offset, poloměr ·S, úhly beze změny (transformace je
|
||||
/// uniformní scale + posun, bez rotace/zrcadlení).</summary>
|
||||
private Edge T(Edge e) => e.Kind == EdgeKind.Line
|
||||
? Edge.Line(X(e.SX), Y(e.SY), X(e.EX), Y(e.EY))
|
||||
: Edge.Arc(X(e.SX), Y(e.SY), X(e.EX), Y(e.EY), X(e.Cx), Y(e.Cy), e.R * _s, e.A0, e.Sweep);
|
||||
public void Stroke(Contour c)
|
||||
{
|
||||
if (_zachovavaOblouky) { _inner.Stroke(new Contour(c.Edges.Select(T).ToList(), c.Closed)); return; }
|
||||
_inner.Polyline(Zplosti(c), c.Closed);
|
||||
}
|
||||
|
||||
/// <summary>Přemapuje hranu (jen pro transformace zachovávající oblouky): body afinně,
|
||||
/// poloměr ·měřítko, počáteční úhel o natočení, rozsah beze změny.</summary>
|
||||
private Edge T(Edge e)
|
||||
{
|
||||
if (e.Kind == EdgeKind.Line)
|
||||
return Edge.Line(X(e.SX, e.SY), Y(e.SX, e.SY), X(e.EX, e.EY), Y(e.EX, e.EY));
|
||||
|
||||
return Edge.Arc(
|
||||
X(e.SX, e.SY), Y(e.SX, e.SY), X(e.EX, e.EY), Y(e.EX, e.EY),
|
||||
X(e.Cx, e.Cy), Y(e.Cx, e.Cy), e.R * _meritko, e.A0 + _uhelDeg * Math.PI / 180, e.Sweep);
|
||||
}
|
||||
|
||||
private List<(double X, double Y)> Zplosti(Contour c)
|
||||
{
|
||||
var body = new List<(double X, double Y)>();
|
||||
foreach (var e in c.Edges) e.AppendPoints(body, 0.02);
|
||||
if (c.Edges.Count > 0) body.Add((c.Edges[^1].EX, c.Edges[^1].EY));
|
||||
return body.Select(p => (X(p.X, p.Y), Y(p.X, p.Y))).ToList();
|
||||
}
|
||||
|
||||
public void Text(double x, double y, string text, double heightMm,
|
||||
HAlign h = HAlign.Left, VAlign v = VAlign.Baseline, double rotationDeg = 0,
|
||||
TextStyle style = default, double widthFactor = 1, double maxWidthMm = 0)
|
||||
=> _inner.Text(X(x), Y(y), text, heightMm * _s, h, v, rotationDeg, style, widthFactor, maxWidthMm * _s);
|
||||
=> _inner.Text(X(x, y), Y(x, y), text, heightMm * _meritko, h, v, rotationDeg + _uhelDeg,
|
||||
style, widthFactor, maxWidthMm * _meritko);
|
||||
|
||||
public void Fill(IReadOnlyList<Contour> loops)
|
||||
=> _inner.Fill(loops.Select(c => new Contour(c.Edges.Select(T).ToList(), c.Closed)).ToList());
|
||||
public void Fill(IReadOnlyList<Contour> loops, Vypln vypln = default)
|
||||
=> _inner.Fill(
|
||||
loops.Select(c => new Contour(
|
||||
_zachovavaOblouky ? c.Edges.Select(T).ToList() : Usecky(c), c.Closed)).ToList(),
|
||||
Vypln(vypln));
|
||||
|
||||
/// <summary>Přemapuje výplň spolu s geometrií. Šrafa se musí zmenšit se svou plochou –
|
||||
/// jinak logo zmenšené do rámečku dostane vzor v původní rozteči a v CADu z něj vyjde
|
||||
/// buď plná plocha, nebo pár čar přes celé logo.</summary>
|
||||
private Vypln Vypln(Vypln v)
|
||||
{
|
||||
if (v.JeSolid) return v;
|
||||
return v with
|
||||
{
|
||||
Meritko = v.Meritko * _meritko,
|
||||
UhelDeg = v.UhelDeg + _uhelDeg,
|
||||
Cary = v.Cary?.Select(u => new Usecka(X(u.X1, u.Y1), Y(u.X1, u.Y1), X(u.X2, u.Y2), Y(u.X2, u.Y2))).ToList(),
|
||||
Definice = v.Definice?.Select(Vzor).ToList(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Definiční čára vzoru: vztažný bod je bod (celá transformace), rozteč je vektor
|
||||
/// (jen lineární část, bez posunu), délky čárek se škálují, směr se otočí.</summary>
|
||||
private VzorCara Vzor(VzorCara c) => c with
|
||||
{
|
||||
UhelDeg = c.UhelDeg + _uhelDeg,
|
||||
Zaklad = (X(c.Zaklad.X, c.Zaklad.Y), Y(c.Zaklad.X, c.Zaklad.Y)),
|
||||
Offset = (_a * c.Offset.X + _c * c.Offset.Y, _b * c.Offset.X + _d * c.Offset.Y),
|
||||
Carky = c.Carky.Select(d => d * _meritko).ToList(),
|
||||
};
|
||||
|
||||
/// <summary>Obrys rozložený na úsečky (pro transformace, které oblouk nezachovají).</summary>
|
||||
private List<Edge> Usecky(Contour c)
|
||||
{
|
||||
var body = Zplosti(c);
|
||||
var hrany = new List<Edge>(Math.Max(body.Count - 1, 0));
|
||||
for (int i = 0; i + 1 < body.Count; i++)
|
||||
hrany.Add(Edge.Line(body[i].X, body[i].Y, body[i + 1].X, body[i + 1].Y));
|
||||
return hrany;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,20 +215,14 @@ public sealed class MainViewModel : ObservableObject
|
||||
public IReadOnlyDictionary<string, string> ResolveLoga(Priloha r)
|
||||
=> LogaResolver.Resolve(Projekt, r, Ciselniky, RzpSlozka);
|
||||
|
||||
/// <summary>Jména DXF souborů ve složce <c>loga\</c> vedle .exe (prázdná položka = „žádné") –
|
||||
/// nabídka pro výběr log financujících institucí v Údajích projektu.</summary>
|
||||
/// <summary>Jména DXF log z obou složek <c>loga\</c> – uživatelské i dodávané (prázdná položka
|
||||
/// = „žádné") – nabídka pro výběr log financujících institucí v Údajích projektu.</summary>
|
||||
public IReadOnlyList<string> DostupnaLoga
|
||||
{
|
||||
get
|
||||
{
|
||||
var list = new List<string> { "" };
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(LogaResolver.LogaDir))
|
||||
list.AddRange(Directory.GetFiles(LogaResolver.LogaDir, "*.dxf")
|
||||
.Select(Path.GetFileName)!);
|
||||
}
|
||||
catch { /* nedostupná složka → jen prázdná položka */ }
|
||||
list.AddRange(LogaResolver.DostupnaLoga());
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -311,6 +305,9 @@ public sealed class MainViewModel : ObservableObject
|
||||
public ICommand ExportSeznamuCmd { get; }
|
||||
public ICommand OtevritNapoveduCmd { get; }
|
||||
public ICommand OtevritManualCmd { get; }
|
||||
|
||||
/// <summary>Otevře v Průzkumníku složku, kam si uživatel dává vlastní loga.</summary>
|
||||
public ICommand OtevritSlozkuLogCmd { get; }
|
||||
public ICommand OProgramuCmd { get; }
|
||||
|
||||
public MainViewModel()
|
||||
@@ -407,6 +404,7 @@ public sealed class MainViewModel : ObservableObject
|
||||
ExportSeznamuCmd = new RelayCommand(ExportujSeznamDoXlsx, () => Prilohy.Count > 0);
|
||||
OtevritNapoveduCmd = new RelayCommand(OtevritNapovedu);
|
||||
OtevritManualCmd = new RelayCommand(OtevritManual);
|
||||
OtevritSlozkuLogCmd = new RelayCommand(OtevritSlozkuLog);
|
||||
OProgramuCmd = new RelayCommand(OProgramu);
|
||||
|
||||
SledujZmeny();
|
||||
@@ -836,6 +834,7 @@ public sealed class MainViewModel : ObservableObject
|
||||
_exportZrusen = false;
|
||||
int hotovo = 0, bezJmena = 0, n = prilohy.Count, index = 0;
|
||||
var chyby = new List<string>();
|
||||
var pouziteLoga = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
try
|
||||
{
|
||||
foreach (var p in prilohy)
|
||||
@@ -855,7 +854,9 @@ public sealed class MainViewModel : ObservableObject
|
||||
|
||||
try
|
||||
{
|
||||
renderer.ExportPdf(Projekt, p, Revize, Ciselniky, cesta, ResolveLoga(p));
|
||||
var loga = ResolveLoga(p);
|
||||
pouziteLoga.UnionWith(loga.Values);
|
||||
renderer.ExportPdf(Projekt, p, Revize, Ciselniky, cesta, loga);
|
||||
hotovo++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -870,7 +871,8 @@ public sealed class MainViewModel : ObservableObject
|
||||
StavovaZprava = "Připraveno";
|
||||
}
|
||||
|
||||
ZobrazSouhrnExportu(cilRozpisky, hotovo, bezJmena, chyby);
|
||||
ZobrazSouhrnExportu(cilRozpisky, hotovo, bezJmena, chyby,
|
||||
RozpiskaRenderer.NepodporovanePrvky(pouziteLoga));
|
||||
}
|
||||
|
||||
/// <summary>Mezera mezi rozpiskami v dlaždicovém DXF (výkresové jednotky).</summary>
|
||||
@@ -914,6 +916,7 @@ public sealed class MainViewModel : ObservableObject
|
||||
int cols = Math.Min(DxfSloupcu, Math.Max(1, n));
|
||||
var dxf = new DxfProfileRenderer();
|
||||
var chyby = new List<string>();
|
||||
var pouziteLoga = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < n; i++)
|
||||
@@ -927,8 +930,10 @@ public sealed class MainViewModel : ObservableObject
|
||||
double oy = -row * (cell.H + DxfMezera); // další řádky pod sebe (osa Y nahoru)
|
||||
try
|
||||
{
|
||||
var loga = ResolveLoga(prilohy[i]);
|
||||
pouziteLoga.UnionWith(loga.Values);
|
||||
var draw = renderer.BuildDraw(Projekt, prilohy[i], Revize, Ciselniky, vyplnene: true,
|
||||
ResolveLoga(prilohy[i]), ramecekA4: true);
|
||||
loga, ramecekA4: true);
|
||||
// Generálka naprázdno: výjimka uprostřed kreslení by ve sdíleném dokumentu
|
||||
// nechala torzo rozpisky (entity přidané před pádem nejde vzít zpět). Do
|
||||
// skutečného DXF se tak kreslí až kresba, která celá prošla.
|
||||
@@ -952,6 +957,7 @@ public sealed class MainViewModel : ObservableObject
|
||||
StavovaZprava = "Připraveno";
|
||||
return;
|
||||
}
|
||||
// Verzi výstupu drží DxfProfileRenderer (natvrdo, ať ji neurčí default balíčku).
|
||||
DxfWriter.Write(cesta, dxf.Document, binary: true); // binární DXF = výrazně menší soubor
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -973,6 +979,7 @@ public sealed class MainViewModel : ObservableObject
|
||||
sb.AppendLine($"Chyby: {chyby.Count}");
|
||||
foreach (var ch in chyby) sb.AppendLine($" • {ch}");
|
||||
}
|
||||
PripojNepodporovane(sb, RozpiskaRenderer.NepodporovanePrvky(pouziteLoga));
|
||||
sb.AppendLine();
|
||||
sb.Append($"Soubor: {cesta}");
|
||||
MessageBox.Show(sb.ToString(), "Export do DXF dokončen", MessageBoxButton.OK,
|
||||
@@ -1202,7 +1209,8 @@ public sealed class MainViewModel : ObservableObject
|
||||
}
|
||||
|
||||
/// <summary>Souhrn dávkového exportu (počet hotových, přeskočených bez jména, chyby) + cílová složka.</summary>
|
||||
private static void ZobrazSouhrnExportu(string cilRozpisky, int hotovo, int bezJmena, IReadOnlyList<string> chyby)
|
||||
private static void ZobrazSouhrnExportu(string cilRozpisky, int hotovo, int bezJmena,
|
||||
IReadOnlyList<string> chyby, IReadOnlyList<string> nepodporovane)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine($"Vyexportováno do PDF: {hotovo}");
|
||||
@@ -1212,6 +1220,7 @@ public sealed class MainViewModel : ObservableObject
|
||||
sb.AppendLine($"Chyby: {chyby.Count}");
|
||||
foreach (var ch in chyby) sb.AppendLine($" • {ch}");
|
||||
}
|
||||
PripojNepodporovane(sb, nepodporovane);
|
||||
sb.AppendLine();
|
||||
sb.Append($"Složka: {cilRozpisky}");
|
||||
|
||||
@@ -1219,6 +1228,16 @@ public sealed class MainViewModel : ObservableObject
|
||||
chyby.Count > 0 ? MessageBoxImage.Warning : MessageBoxImage.Information);
|
||||
}
|
||||
|
||||
/// <summary>Přidá do souhrnu upozornění na prvky vložených DXF, které kreslítko neumí.
|
||||
/// Export se kvůli nim nezastaví – jen se nevykreslí, a o tom se uživatel musí dozvědět.</summary>
|
||||
private static void PripojNepodporovane(System.Text.StringBuilder sb, IReadOnlyList<string> nepodporovane)
|
||||
{
|
||||
if (nepodporovane.Count == 0) return;
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("Ve vložených DXF jsou prvky, které program neumí vykreslit:");
|
||||
foreach (var radek in nepodporovane) sb.AppendLine($" • {radek}");
|
||||
}
|
||||
|
||||
/// <summary>Vybere DXF orientačního schématu pro vybranou přílohu. Leží-li soubor ve složce
|
||||
/// aktuálního .rzp, uloží se relativně (jen jméno), jinak absolutní cestou.</summary>
|
||||
private void VyberSchema()
|
||||
@@ -1399,4 +1418,21 @@ public sealed class MainViewModel : ObservableObject
|
||||
"Nápověda", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Otevře v Průzkumníku složku pro uživatelská loga (a založí ji, když ještě není).
|
||||
/// Vlastní loga patří sem, ne do složky programu – tu přeinstalace přepíše.</summary>
|
||||
private void OtevritSlozkuLog()
|
||||
{
|
||||
var slozka = LogaResolver.UzivatelskaLogaDir;
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(slozka);
|
||||
Process.Start(new ProcessStartInfo(slozka) { UseShellExecute = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show("Složku s logy se nepodařilo otevřít:\n" + slozka + "\n\n" + ex.Message,
|
||||
"Loga", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<UserControl x:Class="Rozpisky.Views.NastaveniView"
|
||||
<UserControl x:Class="Rozpisky.Views.NastaveniView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
@@ -28,6 +28,7 @@
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Kořenová cesta exportu" Style="{StaticResource FieldLabel}"/>
|
||||
@@ -39,6 +40,14 @@
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="DXF šablona" Style="{StaticResource FieldLabel}"/>
|
||||
<TextBox Grid.Row="2" Grid.Column="1" Margin="{StaticResource Inp}"
|
||||
Text="{Binding Nastaveni.SablonaDxf, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="Vlastní loga" Style="{StaticResource FieldLabel}"/>
|
||||
<StackPanel Grid.Row="3" Grid.Column="1" Margin="{StaticResource Inp}">
|
||||
<Button Content="Otevřít složku s logy" HorizontalAlignment="Left" Padding="10,0"
|
||||
Command="{Binding OtevritSlozkuLogCmd}"/>
|
||||
<TextBlock Style="{StaticResource Text.Caption}" TextWrapping="Wrap" Margin="0,4,0,0"
|
||||
Text="Sem patří DXF loga, která si přidáš sám (třeba k nové firmě v číselníku). Přeinstalace programu na ně nesahá a v nabídce log mají přednost před dodávanými."/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<UserControl x:Class="Rozpisky.Views.UdajeProjektuView"
|
||||
<UserControl x:Class="Rozpisky.Views.UdajeProjektuView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
@@ -146,7 +146,7 @@
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="5" Style="{StaticResource Text.Caption}" Margin="0,0,0,6"
|
||||
Text="Loga dotačních programů (vlevo) a státních institucí (vpravo). DXF soubory ze složky loga\ vedle programu."/>
|
||||
Text="Loga dotačních programů (vlevo) a státních institucí (vpravo). DXF soubory dodávané s programem i vlastní (Nastavení → Otevřít složku s logy)."/>
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Logo dotační (vlevo)" Style="{StaticResource FieldLabel}"/>
|
||||
<ComboBox Grid.Row="1" Grid.Column="1" Margin="{StaticResource Inp}"
|
||||
|
||||
@@ -147,6 +147,15 @@ Na revize se odkazují řádkové revizní sloupce přílohy (R1–R4) a revizn
|
||||
> mu je nesmí přepsat** – zároveň smí nová verze dodávaný číselník libovolně měnit.
|
||||
> Klíč položky: `Nazev` (organizace, firmy, kraje), `Hodnota` (jména, verze), `Oznaceni` (části);
|
||||
> porovnává se bez ohledu na velikost písmen.
|
||||
>
|
||||
> **Skrytí se neruší smazáním.** Uživatelská vrstva nedrží historii operací, jen výsledný rozdíl.
|
||||
> Přejmenování dodávané položky = její klíč do `Skryte` + nový záznam do `Vlastni`; smazáním toho
|
||||
> nového záznamu ubude jen druhá půlka a původní dodávaná položka zůstane skrytá. Není jak rozlišit
|
||||
> „přejmenoval jsem a pak smazal" od „smazal jsem dodávanou položku" – obojí končí stejným stavem.
|
||||
> Zpět ji vrátí **zapsání téže hodnoty znovu**: klíč se objeví ve stavu, vypadne ze `Skryte`,
|
||||
> a shoduje-li se obsah s dodávaným, nezapíše se ani do `Vlastni` – záznam je tím opět plně pod
|
||||
> správou programu. U vícpolových číselníků (organizace, firmy) to znamená trefit **všechna** pole,
|
||||
> jinak zůstane jako vlastní záznam a nová verze programu mu už data neaktualizuje.
|
||||
|
||||
### 3.5 Konfigurace
|
||||
|
||||
@@ -341,6 +350,12 @@ v samostatné aplikaci.
|
||||
staveb. **Není to** pole `K1`–`K47` (to je strojový kód, viz [§ 3.7](#37-kódové-označení-přílohy-47-pozic-pole-k1k47)).
|
||||
- Pole pro **otisk razítek** / razítko oprávněné osoby (pole 3, kap. 4.3.3) zůstává v elektronické
|
||||
verzi obvykle prázdné.
|
||||
- **Kde loga leží** – ve dvou složkách `loga\`, stejná hierarchie jako u číselníků
|
||||
([3.4](#34-číselníky)): dodávaná vedle `.exe` a uživatelská v `%APPDATA%\Rozpisky\loga`, kterou
|
||||
přeinstalace nepřepisuje. Hledá se nejdřív mezi uživatelskými, takže logo stejného jména přebíjí
|
||||
dodávané. Nabídka v Údajích projektu je sjednocením obou; složku otevře tlačítko
|
||||
v Nastavení. Bez toho by uživatel, který si do číselníku přidá firmu s vlastním logem, musel
|
||||
soubor kopírovat do instalační složky – a o to logo při aktualizaci přijít.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+85
-15
@@ -32,8 +32,13 @@ zvláštní `PlatformTarget` se nenastavuje.
|
||||
|
||||
### Zápis DXF – co je potřeba vědět
|
||||
|
||||
- **Tloušťka čáry** = `Layer.LineWeight` (enum, hodnota v 1/100 mm – standardní hodnoty:
|
||||
`0,5,9,13,15,18,20,25,30,35,40,50,53,60,70,80,90,100,106,120,140,158,200,211`).
|
||||
- **Tloušťka čáry** je enum v 1/100 mm (standardní hodnoty
|
||||
`0,5,9,13,15,18,20,25,30,35,40,50,53,60,70,80,90,100,106,120,140,158,200,211`); zapisuje se
|
||||
na hladinu (`Layer.LineWeight`) i na entitu, která ji přebíjí. Milimetry z kreslicího rozhraní
|
||||
se přichytí k nejbližší standardní hodnotě.
|
||||
- **Barva** se zapisuje ve stejném druhu, v jakém přišla ze zdroje: indexovaná číslem (group 62),
|
||||
přímá jako RGB (group 420), `ByLayer`/`ByBlock` odkazem. Zplošťování na RGB by výkres v CADu
|
||||
odřízlo od tabulky barev.
|
||||
- **Diakritika** se do DXF ukládá jako Unicode escape `\U+XXXX` (např. `š` → `\U+0161`) – AutoCAD
|
||||
i ostatní čtečky to čtou správně.
|
||||
- Pole, která byla v šabloně `ATTDEF`, se při vyplnění zapisují jako obyčejné `TEXT`/`MTEXT` entity
|
||||
@@ -50,13 +55,16 @@ Aplikace je členěná dle vrstev (MVVM + služby):
|
||||
```
|
||||
Rozpisky/
|
||||
├─ Rendering/ ← kreslicí vrstva (nezávislá na cíli)
|
||||
│ ├─ IProfileRenderer.cs ← kreslicí rozhraní + HAlign/VAlign + TextStyle
|
||||
│ ├─ IProfileRenderer.cs ← kreslicí rozhraní + HAlign/VAlign + TextStyle + Vypln
|
||||
│ ├─ SkiaProfileRenderer.cs ← výstup přes SkiaSharp (obrazovka / PNG / PDF)
|
||||
│ ├─ DxfProfileRenderer.cs ← výstup do DXF (ACadSharp)
|
||||
│ ├─ BoundsRenderer.cs ← spočítá bbox kresby (fit / umístění na A4)
|
||||
│ └─ TransformRenderer.cs ← dekorátor: měřítko + posun (fit fragmentů/loga)
|
||||
│ ├─ TransformRenderer.cs ← dekorátor: afinní transformace (fit loga, rozvinutí bloku)
|
||||
│ ├─ KresliciStav.cs ← barva s původem (ACI/RGB/ByLayer), hladina, typ čáry, tloušťka
|
||||
│ └─ AciPaleta.cs ← indexované barvy AutoCADu (ACI 0–255) → RGB
|
||||
├─ Dxf/ ← čtení šablony, mapování hodnot, kódy
|
||||
│ ├─ DxfTemplate.cs ← načte DXF šablonu a přehraje ji do IProfileRenderer
|
||||
│ ├─ SrafaVzor.cs ← rozvine vzor šrafy (ANSI31 apod.) na čáry
|
||||
│ ├─ RozpiskaRenderer.cs ← spojuje šablonu + hodnoty + exportéry (fasáda pro UI)
|
||||
│ ├─ RozpiskaValues.cs ← „tag DXF → hodnota" z projektu/přílohy/číselníků (reflexí dle mapovani.json)
|
||||
│ ├─ MapovaniStore.cs ← načtení mapování z mapovani.json (config vedle .exe)
|
||||
@@ -78,7 +86,7 @@ Datové soubory jsou rozdělené podle toho, kdo je vlastní:
|
||||
| Kde | Co | Kdo zapisuje |
|
||||
|---|---|---|
|
||||
| vedle `.exe` (zdroj `Rozpisky/Podklady/`) | `Rozpiska.dxf`, `SEZNAM.xlsx`, `ciselniky.vychozi.json`, `mapovani.json`, `loga/`, manuál SŽ (PDF) | nikdo – dodává se s programem, každá instalace přepíše |
|
||||
| `%APPDATA%\Rozpisky` ([`UzivatelskaData`](../Rozpisky/Data/UzivatelskaData.cs)) | `ciselniky.user.json`, `cad.json`, `layout.json`, `error.log`, `tisk_diag.log` | aplikace za běhu |
|
||||
| `%APPDATA%\Rozpisky` ([`UzivatelskaData`](../Rozpisky/Data/UzivatelskaData.cs)) | `ciselniky.user.json`, `cad.json`, `layout.json`, `error.log`, `tisk_diag.log`, `loga/` (vlastní loga uživatele) | aplikace a uživatel |
|
||||
| kdekoli u uživatele | projekty `.rzp` | aplikace na vyžádání |
|
||||
|
||||
Rozdělení existuje proto, aby přeinstalace novou verzí (nebo `.exe` v `Program Files`, kam se nesmí
|
||||
@@ -95,37 +103,97 @@ Vše se kreslí ve **výkresových milimetrech**, osa **Y nahoru** (jako v CAD/D
|
||||
public enum HAlign { Left, Center, Right }
|
||||
public enum VAlign { Baseline, Bottom, Middle, Top }
|
||||
|
||||
public readonly record struct TextStyle(string? FontFamily = null, bool Bold = false,
|
||||
bool Italic = false, string? FontFile = null);
|
||||
public enum BarvaDruh { ByLayer, ByBlock, Index, True }
|
||||
public readonly record struct Barva(BarvaDruh Druh, byte Index, int Rgb);
|
||||
public readonly record struct HladinaStav(string Nazev, Barva Barva, string TypCary = "Continuous",
|
||||
double TloustkaMm = 0, bool Tisknout = true, bool Zapnuta = true);
|
||||
public readonly record struct KresliciStav(HladinaStav Hladina, Barva Barva,
|
||||
string? TypCary = null, double? TloustkaMm = null);
|
||||
|
||||
public readonly record struct TextStyle(string? FontFamily = null, bool Bold = false, bool Italic = false,
|
||||
string? FontFile = null, double ObliqueDeg = 0,
|
||||
string? Nazev = null, bool JeShx = false);
|
||||
|
||||
public readonly record struct Usecka(double X1, double Y1, double X2, double Y2);
|
||||
public readonly record struct Vypln(string? Vzor = null, double Meritko = 1, double UhelDeg = 0,
|
||||
IReadOnlyList<Usecka>? Cary = null);
|
||||
|
||||
public interface IProfileRenderer
|
||||
{
|
||||
void Layer(string name, int rgb, double lineWeightMm = 0.25); // barva 0xRRGGBB, tloušťka ByLayer
|
||||
void Stav(in KresliciStav s);
|
||||
void Line(double x1, double y1, double x2, double y2);
|
||||
void Polyline(IReadOnlyList<(double X, double Y)> pts, bool closed = false);
|
||||
void Circle(double cx, double cy, double r);
|
||||
void Stroke(Contour c); // obrys se zachovanými oblouky
|
||||
void Text(double x, double y, string text, double heightMm,
|
||||
HAlign h = HAlign.Left, VAlign v = VAlign.Baseline, double rotationDeg = 0,
|
||||
TextStyle style = default, double widthFactor = 1, double maxWidthMm = 0);
|
||||
void Fill(IReadOnlyList<IReadOnlyList<(double X, double Y)>> loops); // HATCH výplně (even-odd)
|
||||
void Fill(IReadOnlyList<Contour> loops, Vypln vypln = default); // HATCH výplně (even-odd)
|
||||
}
|
||||
```
|
||||
|
||||
- `rgb` je barva `0xRRGGBB` rozbalená z DXF (ByLayer i index/true-color). Bílá se kreslí černě
|
||||
(bílá na bílém papíře by zmizela).
|
||||
**Proč `KresliciStav` a ne `int rgb`.** Šablona i všech 9 log jsou zapsané výhradně
|
||||
**indexovanými barvami AutoCADu** (group 62). Kdyby rozhraní neslo jen výsledné RGB, export by
|
||||
z nich udělal natvrdo true color (group 420), výkres by v CADu přestal reagovat na tabulku barev
|
||||
a `ByLayer` by se rozpadlo na kopie barvy hladiny. Barva proto nese **původ**, ne jen hodnotu:
|
||||
rastrové renderery si vyžádají hotové RGB přes `RgbProKresbu()`, DXF export zapíše původní druh.
|
||||
Ze stejného důvodu se přenáší celý popis hladiny – ve výstupu se staví z tabulky hladin, ne
|
||||
z první entity, která na ní přistála.
|
||||
|
||||
- **Paleta ACI** ([`AciPaleta`](../Rozpisky/Rendering/AciPaleta.cs)) je vlastní, ne z ACadSharp:
|
||||
ta se od AutoCADu liší u 33 indexů, mimo jiné u 8 (v šabloně použitý) a u posledního indexu
|
||||
většiny desítek odstínů, kde vrací barvu ze sousední skupiny (29 a 39 mají dokonce tutéž).
|
||||
Rozsah 10–249 se dopočítává, ne vypisuje: 24 odstínů × 5 jasů × 2 sytosti.
|
||||
- `TextStyle.Nazev` = jméno stylu ve zdroji („Popis základní"). Nese se dál, aby ho export zapsal
|
||||
beze změny – jeden zdrojový styl = jeden styl ve výstupu.
|
||||
- `widthFactor` = horizontální měřítko písma (DXF width factor). `maxWidthMm` = šířka pro zalomení
|
||||
víceřádkového textu (0 = jen explicitní zlomy).
|
||||
- **Bílá se kreslí černě** (bílá na bílém papíře by zmizela) – jen v rastrových rendererech;
|
||||
do DXF jde barva beze změny.
|
||||
|
||||
Implementace:
|
||||
|
||||
- **`SkiaProfileRenderer`** – kreslí na `SKCanvas`. Dostane funkci `Map(mmX, mmY) → bod plátna`
|
||||
a `pxPerMm`. Text vzpřímeně (rotace opačně, plátno má Y dolů), fonty ze stylu/souboru s fallbackem.
|
||||
Je `IDisposable` (uvolní typefaces) – používat přes `using`.
|
||||
- **`DxfProfileRenderer`** – pro každou hladinu založí `Layer` (barva + tloušťka), entity přidává do
|
||||
`Document`. Výsledek zapíše `DxfWriter.Write(path, renderer.Document, binary: true)`.
|
||||
- **`DxfProfileRenderer`** – staví ACadSharp entity do `Document`; hladiny, textové styly i barvy
|
||||
podle zdroje. Verze výstupu je zafixovaná (`VerzeVystupu`), ať ji neurčí default balíčku.
|
||||
Výsledek zapíše `DxfWriter.Write(path, renderer.Document, binary: true)`.
|
||||
- **`BoundsRenderer`** – nic nekreslí, jen sčítá min/max souřadnic (fit / vystředění na A4).
|
||||
- **`TransformRenderer`** – dekorátor: na souřadnice aplikuje uniformní měřítko + posun. Slouží
|
||||
k umístění vkládaného DXF fragmentu (logo / schéma) do vymezeného rámečku.
|
||||
- **`TransformRenderer`** – dekorátor s afinní transformací: měřítko + posun (umístění loga do
|
||||
rámečku), nebo měřítko os + natočení + posun (rozvinutí bloku, `ProBlok`). Oblouk zůstane
|
||||
obloukem jen u podobnosti bez zrcadlení – jinak by z kružnice byla elipsa, kterou rozhraní nezná,
|
||||
a tvar se rozloží na lomenou čáru.
|
||||
|
||||
### Které DXF entity se čtou
|
||||
|
||||
| Entita | Jak se kreslí |
|
||||
|---|---|
|
||||
| `LINE`, `CIRCLE`, `ARC` | přímo; oblouk analyticky, hladký v každém přiblížení |
|
||||
| `LWPOLYLINE`, `POLYLINE` (2D) | lomená čára, s `bulge` jako obrys s kruhovými oblouky |
|
||||
| `POLYLINE` (3D) | promítnuto do roviny XY |
|
||||
| `SPLINE` | lomená čára, dílek ≈ 0,3 mm podle délky řídicího polygonu |
|
||||
| `ELLIPSE` | lomená čára (rozhraní zná jen kruhové oblouky) |
|
||||
| `TEXT`, `MTEXT`, `ATTDEF` | text se stylem, zarovnáním, sklonem a šířkovým faktorem |
|
||||
| `HATCH` | plná výplň, nebo vzor rozvinutý na čáry ([`SrafaVzor`](../Rozpisky/Dxf/SrafaVzor.cs)) |
|
||||
| `SOLID`, `3DFACE` | vyplněný čtyřúhelník (v DXF jsou 3. a 4. roh prohozené) |
|
||||
| `POINT` | tečka |
|
||||
| `INSERT` | rekurzivně rozvinutý blok: vztažný bod, měřítko os, natočení, `ByBlock` barvy |
|
||||
| ostatní | nekreslí se, ale **spočítá se** do `DxfTemplate.NezpracovaneEntity`; souhrn exportu to vypíše |
|
||||
|
||||
### Známá omezení
|
||||
|
||||
- **Vnitřní formátovací kódy MTEXTu** (`\H` výška, `\C` barva, `\f` font, `\S` zlomek) se ztrácejí –
|
||||
čte se `MText.PlainText`, který je odstraňuje. Zvlášť se čte jen `\W` (šířkový faktor), protože
|
||||
ho ACadSharp v `PlainText` nechává a jinak by se vykreslil jako text.
|
||||
- **Zarovnání textu `Aligned` / `Fit`** se aproximuje na `Left` – text se neroztáhne na cílovou délku.
|
||||
- **Typ čáry** se přenáší jako název, ale rastrové renderery kreslí vždy plnou čáru; čárkované styly
|
||||
se projeví až v DXF (v dodávaných souborech je stejně všude `Continuous`).
|
||||
- **Šrafy** se do DXF zapisují i s definičními čarami vzoru (DXF je nese uvnitř entity `HATCH`),
|
||||
takže vypadají stejně jako ve zdroji nezávisle na tom, jaké `.pat` a jednotky má cílový CAD.
|
||||
Měřítko a úhel šrafy zůstávají neutrální (1 a 0) – efektivní hodnoty nese samotná definice.
|
||||
Důvod: ACadSharp při zápisu násobí rozteč vzoru hodnotou `PatternScale`, ale při čtení zpátky
|
||||
nedělí, takže s nenulovým měřítkem by se vzor s každým kolečkem roztáhl.
|
||||
|
||||
---
|
||||
|
||||
@@ -218,6 +286,8 @@ Aplikace **nemá CLI režim** – vše jede z GUI. Neočekávané výjimky zachy
|
||||
dodávaný číselník měnit, aniž by uživatel přišel o svoje záznamy.
|
||||
- **Migrace starých poloh** ([`Migrace`](../Rozpisky/Data/Migrace.cs)) – data zapisovaná dřív vedle
|
||||
.exe se při startu přenesou do uživatelské složky; originál se jen přejmenuje na `.migrovano`.
|
||||
- **Loga ve dvou složkách** ([`LogaResolver`](../Rozpisky/Dxf/LogaResolver.cs)) – hledá se nejdřív
|
||||
v uživatelské `loga\`, pak v dodávané; nabídka v UI je sjednocením obou.
|
||||
- **Zamčený cíl exportu** – před zápisem PDF/XLSX/DXF se ověří, zda soubor nedrží jiný program
|
||||
(Excel, prohlížeč PDF), a nabídne Pokračovat/Přerušit.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user