Compare commits

..
10 Commits
Author SHA1 Message Date
marekandClaude Opus 5 a2a93ca155 Agenti: multiagentní provoz se čtyřmi rolemi a pravidla pro git
Hlavní session běží jako orchestrator bez souborových nástrojů a deleguje
na scout, implementer a reviewer. Scout zjišťuje fakta, implementer píše
veškerý kód včetně kreslicího jádra a WPF vrstvy, reviewer dělá revizi
i nezávislý build a testy. Doplněn skill upresni pro nejednoznačná zadání.
Commit až po revizi, push jen na vyžádání.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 06:21:41 +02:00
marekandClaude Opus 5 1e5a552c13 Oprava: exportovaná šrafa šla ven bez definice vzoru a ve špatném měřítku
Nahlásil uživatel: šrafa v exportovaném DXF nedrží měřítko, takže na první
pohled ani nevypadá jako šrafa, a v každém výkresu se chová jinak.

Dvě příčiny:

1. Do výstupu šlo jen *jméno* vzoru (ANSI31), bez definičních čar — ověřeno
   na exportovaném souboru: definicnichCar=0. DXF přitom nese definici vzoru
   uvnitř entity HATCH; bez ní si ji cílový CAD dosadí ze svých .pat, ve svých
   jednotkách a měřítku. Odtud „v každém výkresu jinak". Vypln proto nese
   VzorCara (úhel, vztažný bod, rozteč, čárkování) a export ji zapisuje.

2. Vzor se neškáloval spolu s geometrií. Logo se do rámečku rozpisky vejde jen
   zmenšené (typicky ~0,1×), ale rozteč vzoru zůstávala původní — v CADu z toho
   byla buď plná plocha, nebo pár čar přes celé logo. TransformRenderer teď
   mapuje i definici: vztažný bod celou transformací, rozteč jako vektor (bez
   posunu), délky čárek a měřítko × měřítko transformace.

Měřítko a úhel se do HATCH zapisují neutrální (1 a 0) a efektivní hodnoty nese
definice. ACadSharp totiž při zápisu násobí rozteč hodnotou PatternScale, ale
při čtení zpátky nedělí — s nenulovým měřítkem by se vzor s každým kolečkem
desetkrát roztáhl (změřeno).

Po opravě je rozteč v exportu shodná se zdrojem 1:1 a při zmenšení loga 0,1×
klesne desetkrát, takže počet čar přes logo zůstává stejný. Náhledy všech
9 log jsou pixel po pixelu beze změny. 119 testů zelených.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 15:08:54 +02:00
marekandClaude Opus 5 10548b5698 Doku: kreslicí rozhraní po změnách, tabulka entit a známá omezení
ROZPISKY.md §3 popisuje nové rozhraní (KresliciStav, Vypln, rozšířený TextStyle)
a hlavně *proč* barva nese původ, ne jen RGB. Přibyla tabulka DXF entit, které
se čtou, a seznam známých omezení — inline kódy MTEXTu, zarovnání Aligned/Fit,
typy čar, dohledávání vzoru šrafy v CADu. Opraveno tvrzení, že tloušťka čáry je
jen Layer.LineWeight; zapisuje se i na entitu.

CLAUDE.md doplněno druhé pravidlo architektury: co má přežít cestu DXF → model
→ DXF, musí protéct rozhraním — obcházet ho zpátky na CadDocument šablony je
porušení toho prvního. Baseline testů 57 → 114 (i v subagentech).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 14:49:34 +02:00
marekandClaude Opus 5 a900acc7bb Entity: rozvinout bloky, doplnit elipsy a hlásit, co program neumí
Switch v Replay neměl default větev: INSERT, ELLIPSE, POINT, SOLID, 3DFACE
a POLYLINE3D propadly bez akce a bez záznamu. V dodávaných souborech žádná
z nich není, ale uživatel si přidává vlastní loga – to nejhorší, co se mu může
stát, je logo, které se nevykreslí a nikdo mu to neřekne.

- INSERT se rozvine rekurzivně (vztažný bod, měřítko os, natočení), s ochranou
  proti bloku vkládajícímu sám sebe. Uvnitř bloku se hladina „0" a barva
  ByBlock berou od vkládající entity – proto jde stejný blok vložit dvakrát
  a pokaždé v jiné barvě.
- TransformRenderer umí celou afinní transformaci, ne jen měřítko a posun.
  Oblouk zůstane obloukem jen u podobnosti bez zrcadlení; jinak by z kružnice
  byla elipsa, kterou kreslicí rozhraní nezná, tak se rozloží na lomenou čáru.
- ELLIPSE, POINT, SOLID, 3DFACE, POLYLINE3D se kreslí. U SOLID pozor na
  prohozený 3. a 4. roh, jinak vyjdou přesýpací hodiny.
- Zbytek se počítá do DxfTemplate.NezpracovaneEntity a souhrn exportu (PDF
  i DXF) ho vypíše. Export se kvůli tomu nezastaví.
- Spline se dělí podle délky (dřív pevných 64 dílků – málo pro dlouhé křivky,
  zbytečně moc pro krátké).
- Verze výstupního DXF se zafixovala na AC1027 (jako zdrojová šablona). Dřív ji
  určoval default ACadSharp, který může upgrade balíčku tiše změnit.

Šablona i všech 9 log projdou s prázdným počítadlem (test). Render log do PNG
je pixel po pixelu shodný s předchozím stavem. 114 testů zelených.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 14:46:02 +02:00
marekandClaude Opus 5 001afc2d81 Šrafy: rozvinout vzor na čáry místo plné výplně
Každá šrafa se kreslila jako plná výplň, bez ohledu na vzor. Logo md_sfdi.dxf
má 3× ANSI31 (šikmé čáry) – vycházel z něj plný blok. Nový SrafaVzor rozvine
definiční čáry vzoru na úsečky: rodina rovnoběžek podle vektoru Offset,
ořezaná hranicemi šrafy sudo-lichou paritou (takže díry ve vnořených smyčkách
vyjdou správně) a čárkováním z DashLengths. Proti zamrznutí náhledu při vadném
měřítku ve zdroji je strop 20 000 úseček na šrafu.

Rozhraní: Fill(loops) dostává druhý parametr Vypln. Rastrové renderery kreslí
rozvinuté čáry (Skia vzory neumí), DXF export je ignoruje a zapíše zpátky HATCH
s původním jménem vzoru, měřítkem a úhlem – v CADu tak zůstane editovatelný.
Když se vzor rozvinout nepodaří, plocha zůstane prázdná; zaplácnout ji načerno
je horší než ji nevyplnit.

Ověřeno i vizuálně: z 9 dodávaných log se změnilo jen md_sfdi, ostatních 8 je
po renderu do PNG pixel po pixelu shodných s předchozím stavem.

105 testů zelených.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 14:36:51 +02:00
marekandClaude Opus 5 95003ce835 Textové styly: zachovat jména ze zdroje a opravit MTEXT
Šablona má styly pojmenované česky („Popis základní", „Název stavby"),
export je zahazoval a vyráběl náhradní jména podle fontu (Verdana, Verdana_B,
Arial_1) – pro každou kombinaci fontu a sklonu nový záznam. Ve správci stylů
v CADu pak nešlo nic najít ani hromadně změnit. Jméno stylu se teď nese
v `TextStyle.Nazev` až do zápisu; jeden zdrojový styl = jeden styl ve výstupu.

Dál:
- SHX styly (vektorové fonty AutoCADu) se přenášejí i s odkazem na soubor a
  příznakem `IsShape`. Kreslí se pořád náhradním fontem – SHX se jako TrueType
  načíst nedá –, ale v exportu zůstane původní styl, ne Standard.
- MTEXT si drží natočení. Nešlo přiřadit `Rotation` (v ACadSharp je jen ke
  čtení), takže se nastavuje směrovým vektorem osy X; natočený víceřádkový
  atribut dřív vycházel vodorovně.
- MTEXT si drží šířkový faktor přes kód \W. Jako vlastnost entity ho nemá.
  Čtečka ho zase odloupne – ACadSharp ho v PlainText nechává, takže bez toho
  by se „W0.8;" vykreslilo jako text.
- Hodnota MTEXTu se escapuje celá (zpětné lomítko, složené závorky), ne jen
  zlomy řádků. Cesta typu C:\projekt v poli dřív rozbila formátování.

99 testů zelených.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 12:18:14 +02:00
marekandClaude Opus 5 d66aec5299 Barvy: zachovat ACI index a ByLayer po celé cestě DXF → model → DXF
Šablona i všech 9 log jsou zapsané výhradně indexovanými barvami AutoCADu
(group 62) – true color v nich není ani jednou. Kreslicí rozhraní přitom
neslo jen `int rgb`, takže se index i ByLayer ztratily už při čtení a export
zapisoval všechno jako natvrdo RGB (group 420). Výkres v CADu pak přestal
reagovat na tabulku barev a barva hladiny se rozpadla na kopie na entitách.

`IProfileRenderer.Layer(name, rgb, mm)` se mění na `Stav(in KresliciStav)`.
Stav nese barvu i s původem (`Barva`: ACI index / true color / ByLayer /
ByBlock), typ čáry, tloušťku a celý popis hladiny (`HladinaStav`) – hladina
se ve výstupu staví z tabulky hladin, ne z první entity, která na ní
přistála. Rámeček `!!NOPRINT` proto zůstane netisknutelný.

Paleta ACI je vlastní (`AciPaleta`), ne z ACadSharp: ta se od AutoCADu liší
u 33 indexů, mimo jiné u 8 (použitý v šabloně) a u posledního indexu většiny
desítek odstínů, kde vrací barvu ze sousední skupiny – 29 a 39 tam dokonce
dostanou tutéž barvu. Rozsah 10–249 se dopočítává, ne vypisuje.

Co ověřují nové testy (93 zelených):
- paleta proti AutoCADu i proti ACadSharp, se seznamem známých odchylek,
- rozřešení ByLayer/ByBlock, tloušťky a typu čáry,
- kolečko šablona → export → načtení: žádná entita není true color, indexy
  odpovídají zdroji, ByLayer zůstane ByLayer, hladiny mají barvu i příznak
  tisku ze zdrojové tabulky.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 12:13:32 +02:00
marekandClaude Opus 5 7e7097c360 Doku: jak se chová skrytí dodávané položky číselníku
Skrytí se neruší smazáním přejmenovaného záznamu (rozdíl nedrží historii
operací) – vrátí ho až zapsání téže hodnoty znovu. U vícepolových číselníků
je potřeba trefit všechna pole, jinak zůstane jako vlastní záznam.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 11:31:54 +02:00
marekandClaude Opus 5 f086e623d6 Oprava: úprava dodávané položky číselníku se tiše ztrácela
Nacti() uložilo dodávanou vrstvu do referenčního snímku _vychozi a tytéž
objekty vložilo do kolekcí, na které se váže mřížka. Úprava buňky tak
mutovala i ten snímek – UlozRozdil pak porovnával stav sám se sebou, rozdíl
vyšel prázdný a oprava dodávané položky zmizela. Mazání a přidávání přitom
fungovalo (tam se reference nesdílí), takže se chyba dobře schovala.

Sluc nově vkládá dodávané položky jako hluboké kopie (serializací, stejně
jako klonování přílohy), takže snímek zůstane netknutý.

Nahlášeno na přejmenování kraje Karlovarský – přesně ten scénář teď hlídají
dva regresní testy; bez opravy oba padají.

Build 0 chyb / 0 varování, 57 testů zelených.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 11:26:10 +02:00
marekandClaude Opus 5 cd2b82db3c Vlastní loga uživatele mimo instalační složku
Loga se dosud hledala jen v loga\ vedle .exe. Kdo si do číselníku přidal
firmu s vlastním logem, musel .dxf nakopírovat do instalační složky – tu ale
přeinstalace přepíše a v Program Files do ní nejde psát. Číselník firmu
přežil, logo ne.

Nově se hledá nejdřív v %APPDATA%\Rozpisky\loga a teprve pak mezi
dodávanými; nabídka v Údajích projektu je sjednocením obou složek (stejné
jméno se nezdvojuje). Tlačítko v Nastavení složku založí a otevře.

Hierarchie je stejná jako u číselníků a je to úmyslné: při konfliktu vyhrává
uživatelská vrstva. Kdyby vyhrávala dodávaná, uživatelova úprava by se tiše
neprojevila – on ji vidí uloženou, program pracuje s něčím jiným. Výjimkou
zůstávají data držící správnost výstupu (mapovani.json, Rozpiska.dxf), kde
vyhrává program.

Subagenti dostali explicitní model: render-dxf a wpf-ui sonnet,
verifikator haiku (jen spouští build a testy).

Build 0 chyb / 0 varování, 55 testů zelených (5 nových na slučování nabídky).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 11:13:13 +02:00
43 changed files with 2590 additions and 286 deletions
+104
View File
@@ -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.
+78
View File
@@ -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.
-55
View File
@@ -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.
+81
View File
@@ -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.
+37
View File
@@ -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é.
-41
View File
@@ -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.
-43
View File
@@ -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.
+13 -8
View File
@@ -1,12 +1,16 @@
--- ---
description: Vyrenderuje všech 9 log do PNG a pošle je uživateli (vizuální kontrola) 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>`, Zadání pro něj (napiš mu ho celé, kontext z téhle konverzace nevidí):
ať se předchozí běh nepřepíše a jde porovnat před/po).
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í 2. Spusť harness `PngHarness.VyrenderujVsechnaLogaDoPng` s cílovou složkou v proměnné prostředí
`ROZPISKY_PNG_OUT`: `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, 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. signalprojekt, tesia, mco, exprojekt+mco, sudopBrno`). Chybějící soubor je regrese nahlas ji.
4. Pošli vzniklé PNG uživateli přes `SendUserFile` (`display: "render"`) s krátkým popiskem. 4. Vrať absolutní cestu ke každému vzniklému PNG, jeden soubor na řádek.
$ARGUMENTS $ARGUMENTS
Pokud `$ARGUMENTS` jmenuje konkrétní logo, pošli po vygenerování jen jeho PNG, ale generuj Až se `implementer` vrátí, pošli uživateli jeho PNG přes `SendUserFile` (`display: "render"`)
vždy všechna — harness běží jako celek. 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.
+3 -2
View File
@@ -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`). `run_in_background: false`).
Zadání pro něj: spusť build celého `Rozpisky.sln` a testovou sadu Zadání pro něj: spusť build celého `Rozpisky.sln` a testovou sadu
+4
View File
@@ -0,0 +1,4 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"agent": "orchestrator"
}
+49
View File
@@ -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.
+66 -10
View File
@@ -11,8 +11,9 @@ dotnet test "Rozpisky.Tests\Rozpisky.Tests.csproj" -nologo -v q
dotnet run --project "Rozpisky\Rozpisky.csproj" 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 Baseline: build bez chyb a varování, **119 testů zelených**. Před hlášením hotovo obojí spustit
(nebo použít `/overit`). (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 ## 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) `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í. 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ý, Z toho plyne druhé pravidlo: **co má přežít cestu DXF → model → DXF, musí protéct rozhraním.**
jen relevantní kapitolu. 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 ## Konvence
@@ -38,8 +46,11 @@ jen relevantní kapitolu.
- Uživatelská data (číselníky, výběr CADu, logy) jdou přes - 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` [`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 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 vrstvách ([`CiselnikyStore`](Rozpisky/Data/CiselnikyStore.cs)) a loga ve dvou složkách
uživatele. Změnu slučování vždy pokrýt testem — chyba tam se projeví tichou ztrátou dat. ([`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ů ## 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` - `docs/SZ_SM011_P10_Manual_v6.md`, `Rozpisky/Podklady/SZ_SM011_P10_Manual_v6.pdf`
— normativní podklad SŽ, jen ke čtení. — normativní podklad SŽ, jen ke čtení.
## Delegování ## Multiagentní provoz
Specializovaní subagenti v `.claude/agents/`: `render-dxf` (Rendering/, Dxf/), `wpf-ui` Repo běží jako tým agentů. `.claude/settings.json` nastavuje `"agent": "orchestrator"`, takže
(ViewModels/, Views/, Themes/), `verifikator` (read-only build + testy). Pro úkol spadající **hlavní session *je* orchestrátor** — má jen `Agent`, `TodoWrite`, `AskUserQuestion` a `Skill`,
celý do jedné z těchto domén je použít; drobnosti napříč vrstvami řešit v hlavním vlákně. žá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 ## Další dokumentace
+87
View File
@@ -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 (250254) 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)));
}
}
+64
View File
@@ -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);
}
}
+43
View File
@@ -178,6 +178,49 @@ public class CiselnikyVrstvyTesty
Assert.Equal(new[] { "Novák", "Černý" }, vysledek.Select(j => j.Hodnota)); 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] [Fact]
public void KlicSeBerePodleTypuPolozky() public void KlicSeBerePodleTypuPolozky()
{ {
+108
View File
@@ -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);
}
}
+194
View File
@@ -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,010,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) { }
}
}
}
+80
View File
@@ -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));
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ public class ObliqueTesty
public void DxfExport_ZachovaObliqueNaTextu() public void DxfExport_ZachovaObliqueNaTextu()
{ {
var dxf = new DxfProfileRenderer(); 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)); 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"); string path = Path.Combine(Path.GetTempPath(), $"rozpisky-oblique-{Guid.NewGuid():N}.dxf");
+2 -2
View File
@@ -84,7 +84,7 @@ public class ObloukyTesty
var contour = new Contour(new[] { arc, line }, Closed: true); var contour = new Contour(new[] { arc, line }, Closed: true);
var g = new DxfProfileRenderer(); var g = new DxfProfileRenderer();
g.Layer("0", 0x000000); g.Stav(KresliciStav.Vychozi);
g.Fill(new[] { contour }); g.Fill(new[] { contour });
// Export → dočasný DXF → import zpět a porovnání bboxu (sagitta ≤ 0,02 mm). // 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 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(); var g = new DxfProfileRenderer();
g.Layer("0", 0); g.Stav(KresliciStav.Vychozi);
g.Stroke(contour); g.Stroke(contour);
var arc = Assert.Single(g.Document.Entities.OfType<Arc>()); var arc = Assert.Single(g.Document.Entities.OfType<Arc>());
+19 -3
View File
@@ -26,9 +26,21 @@ public sealed class RecordingRenderer : IProfileRenderer
public int LineCount { get; private set; } public int LineCount { get; private set; }
public int CircleCount { 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); 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) TextStyle style = default, double widthFactor = 1, double maxWidthMm = 0)
=> Texts.Add(new TextCall(x, y, text, heightMm, h, v, rotationDeg, style, widthFactor, maxWidthMm)); => 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); FillContours.Add(loops);
Vyplne.Add(vypln);
Fills.Add(loops.Select(Zplosti).ToList()); Fills.Add(loops.Select(Zplosti).ToList());
} }
+2 -2
View File
@@ -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 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); 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. // 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)"); 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. // 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); 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)"); Assert.True(filled > 50, $"oblouk se nevykreslil (vyplněno {filled} px)");
} }
} }
+111
View File
@@ -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);
}
}
}
+119
View File
@@ -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);
}
}
+3
View File
@@ -11,6 +11,9 @@ public static class TestData
/// <summary>Plná cesta k logu ve zdrojové složce <c>Rozpisky\Podklady\loga</c>.</summary> /// <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); 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> /// <summary>Všech 9 log dodávaných s programem.</summary>
public static readonly string[] VsechnaLoga = public static readonly string[] VsechnaLoga =
{ {
+102
View File
@@ -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"));
}
}
+13 -2
View File
@@ -164,11 +164,17 @@ public static class CiselnikyStore
}; };
/// <summary>Dodávaná vrstva → odebrat skryté → přepsat/doplnit vlastními. Pořadí dodávaných /// <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) 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 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>()) 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) private static bool StejnyObsah<T>(T a, T b)
=> JsonSerializer.Serialize(a, Opts) == JsonSerializer.Serialize(b, Opts); => 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) private static void Replace<T>(ObservableCollection<T> target, IEnumerable<T> src)
{ {
target.Clear(); target.Clear();
+253 -18
View File
@@ -86,12 +86,34 @@ public sealed partial class DxfTemplate
/// (např. A4 rámeček) pro DXF export jako referenci hranice papíru.</summary> /// (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) 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 _nezpracovane.Clear();
// zdroje (výplně vzadu, popisky vepředu apod.). Dispatch podle typu; odvozené typy (Arc:Circle, Prehraj(g, EntitiesInDrawOrder(), values, includeNoPrint, vlozeny: null, hloubka: 0);
// AttributeDefinition:AttributeBase:TextEntity) musí být ve switchi PŘED svými předky. }
foreach (var e in EntitiesInDrawOrder())
/// <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)
{ {
if (!SetLayer(g, e, includeNoPrint)) continue; foreach (var e in entity)
{
if (!SetLayer(g, e, includeNoPrint, vlozeny)) continue;
switch (e) switch (e)
{ {
case Line l: case Line l:
@@ -110,7 +132,7 @@ public sealed partial class DxfTemplate
// Spliny (NURBS) tesselace na lomenou čáru přes ACadSharp. // Spliny (NURBS) tesselace na lomenou čáru přes ACadSharp.
case Spline sp: 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); if (pts.Count >= 2) g.Polyline(pts, sp.IsClosed);
break; break;
} }
@@ -128,7 +150,7 @@ public sealed partial class DxfTemplate
case Hatch hatch: case Hatch hatch:
{ {
var loops = HatchSmycky(hatch); var loops = HatchSmycky(hatch);
if (loops.Count > 0) g.Fill(loops); if (loops.Count > 0) g.Fill(loops, VyplnZ(hatch, loops));
break; break;
} }
@@ -166,14 +188,115 @@ public sealed partial class DxfTemplate
case MText mt: case MText mt:
{ {
var (h, v) = MapMTextAlign(mt.AttachmentPoint); var (h, v) = MapMTextAlign(mt.AttachmentPoint);
g.Text(mt.InsertPoint.X, mt.InsertPoint.Y, mt.PlainText, mt.Height, h, v, mt.Rotation * RadToDeg, // MTEXT nemá šířkový faktor jako vlastnost entity nese ho buď kód \W
StyleOf(mt.Style), 1, mt.RectangleWidth); // 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; 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>, /// <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 /// 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> /// 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). /// <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čí /// <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> /// (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; var layer = e.Layer;
bool noPrint = JeNoPrint(e); bool noPrint = JeNoPrint(e);
if (noPrint && !includeNoPrint) return false; // vodicí rámeček vkládaného DXF se nekreslí 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; 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 var hladina = HladinaZ(layer);
int lw = (int)e.LineWeight; // LineWeightType: Wnn = nn/100 mm, záporné = ByLayer/Default/ByBlock var barva = BarvaZ(e.Color);
if (lw < 0 && layer is not null) lw = (int)layer.LineWeight; var tloustka = TloustkaZ(e.LineWeight);
g.Layer(layer?.Name ?? "0", rgb, lw > 0 ? lw / 100.0 : 0.18); 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; 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> /// <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( private static (double x, double y, HAlign h, VAlign v) Place(
TextHorizontalAlignment ha, TextVerticalAlignmentType va, XYZ insert, XYZ align) 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; double obliqueRad = entityObliqueRad != 0 ? entityObliqueRad : s?.ObliqueAngle ?? 0;
if (s is null) return new(ObliqueDeg: obliqueRad * RadToDeg); 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 bold = s.TrueType.HasFlag(FontFlags.Bold);
bool italic = s.TrueType.HasFlag(FontFlags.Italic); bool italic = s.TrueType.HasFlag(FontFlags.Italic);
// Rodina písma: z .ttf souboru; když styl soubor nemá (TTF zadané jen jménem), vezmi // 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"). // 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); string? family = ttf is not null ? Path.GetFileNameWithoutExtension(ttf) : FontRodinaZeXData(s);
return new(family, bold, italic, file, obliqueRad * RadToDeg); 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 /// <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; 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> /// <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) private static List<(double X, double Y)> VzorkujEllipse(Hatch.BoundaryPath.Ellipse el)
{ {
+54 -6
View File
@@ -1,4 +1,5 @@
using System.IO; using System.IO;
using Rozpisky.Data;
using Rozpisky.Models; using Rozpisky.Models;
namespace Rozpisky.Dxf; namespace Rozpisky.Dxf;
@@ -8,19 +9,67 @@ namespace Rozpisky.Dxf;
/// Pravidla (kap. 4.3 manuálu SŽ): /// Pravidla (kap. 4.3 manuálu SŽ):
/// <list type="bullet"> /// <list type="bullet">
/// <item><c>Rozpiska_logo1/2</c> financující instituce (dotační vlevo / státní vpravo), /// <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_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_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_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> /// <item><c>Rozpiska_Orientační schéma</c> DXF u přílohy (<c>Priloha.SchemaSoubor</c>), relativně k .rzp.</item>
/// </list> /// </list>
/// Položky bez vyplněného / neexistujícího souboru se vynechají. /// 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> /// </summary>
public static class LogaResolver 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"); 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> /// <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) 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) private static void PridejZeSlozky(IDictionary<string, string> d, string hladina, string? soubor)
{ {
if (string.IsNullOrWhiteSpace(soubor)) return; var cesta = Najdi(soubor);
var cesta = Path.IsPathRooted(soubor) ? soubor : Path.Combine(LogaDir, soubor); if (cesta is not null) d[hladina] = cesta;
if (File.Exists(cesta)) d[hladina] = cesta;
} }
/// <summary>Cesta ke schématu: absolutní beze změny, jinak relativně ke složce .rzp.</summary> /// <summary>Cesta ke schématu: absolutní beze změny, jinak relativně ke složce .rzp.</summary>
+22
View File
@@ -81,6 +81,28 @@ public sealed class RozpiskaRenderer
catch { return null; } // poškozený/nečitelný DXF fragment → bez loga 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, public void ExportPdf(Projekt p, Priloha r, IEnumerable<Revize> revize, Ciselniky c, string path,
IReadOnlyDictionary<string, string>? loga = null) IReadOnlyDictionary<string, string>? loga = null)
=> Exporters.PdfA4(BuildDraw(p, r, revize, c, vyplnene: true, loga), path, frame: _tpl.SheetFrame); => Exporters.PdfA4(BuildDraw(p, r, revize, c, vyplnene: true, loga), path, frame: _tpl.SheetFrame);
+181
View File
@@ -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);
}
}
+98
View File
@@ -0,0 +1,98 @@
namespace Rozpisky.Rendering;
/// <summary>
/// Paleta indexovaných barev AutoCADu (ACI 0255). 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 10249 je dopočítaný, ne vypsaný: 24 odstínů po 15° × 5 jasů, každý ve variantě
/// sytá / poloviční sytost. Krajní hodnoty (09, 250255) 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 250254 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 09. 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 250255 š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 0255 → č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á
/// 1255 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 (10249): 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; // 023 → úhel odstínu 0°, 15°, … 345°
int pozice = (index - 10) % 10; // 09 → 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),
};
}
}
+2 -2
View File
@@ -23,7 +23,7 @@ public sealed class BoundsRenderer : IProfileRenderer
if (y > MaxY) MaxY = y; 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); } 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); 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); foreach (var loop in loops) AddContour(loop);
} }
+140 -20
View File
@@ -14,7 +14,7 @@ namespace Rozpisky.Rendering;
/// (6 primitiv), takže má stejné pokrytí jako vykreslovací engine: složitější DXF entity (spline, /// (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ě. /// 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 /// <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. /// 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 &gt; 0</c>) → <c>MTEXT</c>.</para> /// Jednořádkový text → <c>TEXT</c>, víceřádkový (<c>maxWidthMm &gt; 0</c>) → <c>MTEXT</c>.</para>
/// </summary> /// </summary>
@@ -36,27 +36,46 @@ public sealed class DxfProfileRenderer : IProfileRenderer
_doc.Layers.Add(_aktualniVrstva); _doc.Layers.Add(_aktualniVrstva);
} }
_vrstvy[_aktualniVrstva.Name] = _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> /// <summary>Hotový výkres se všemi rozpiskami (pro <c>DxfWriter.Write</c>).</summary>
public CadDocument Document => _doc; 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; _aktualniVrstva = NajdiVrstvu(s.Hladina);
if (!_vrstvy.TryGetValue(name, out var vrstva)) _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) }; vrstva = new AcLayer(name);
_doc.Layers.Add(vrstva); _doc.Layers.Add(vrstva);
} }
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; _vrstvy[name] = vrstva;
} return vrstva;
_aktualniVrstva = vrstva;
_aktualniBarva = RgbNaColor(rgb); // barva entity dle zdroje (ByLayer už rozbalil DxfTemplate)
_aktualniTloustka = SnapTloustka(lineWeightMm);
} }
public void Line(double x1, double y1, double x2, double y2) public void Line(double x1, double y1, double x2, double y2)
@@ -111,9 +130,11 @@ public sealed class DxfProfileRenderer : IProfileRenderer
{ {
var mtext = new MText var mtext = new MText
{ {
Value = text.Replace("\r\n", "\\P").Replace("\n", "\\P"), Value = MTextHodnota(text, widthFactor),
InsertPoint = new XYZ(x, y, 0), InsertPoint = new XYZ(x, y, 0),
Height = heightMm, Height = heightMm,
// MTEXT nemá zapisovatelné natočení nese ho směrový vektor osy X (group 11).
AlignmentPoint = SmerOsyX(rotationDeg),
RectangleWidth = maxWidthMm, RectangleWidth = maxWidthMm,
AttachmentPoint = MapAttach(h, v), 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; if (loops.Count == 0) return;
try 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) foreach (var loop in loops)
{ {
if (loop.Edges.Count == 0) continue; 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). /// <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 /// 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> /// 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> /// stylu (použije se výchozí styl dokumentu).</summary>
private AcTextStyle? StylProEntitu(TextStyle s) 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"); 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; if (_styly.TryGetValue(klic, out var styl)) return styl;
var ff = FontFlags.Regular; var ff = FontFlags.Regular;
if (s.Bold) ff |= FontFlags.Bold; if (s.Bold) ff |= FontFlags.Bold;
if (s.Italic) ff |= FontFlags.Italic; 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. // 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); _doc.TextStyles.Add(styl);
_styly[klic] = styl; _styly[klic] = styl;
return styl; return styl;
@@ -246,8 +358,16 @@ public sealed class DxfProfileRenderer : IProfileRenderer
return kandidat; return kandidat;
} }
private static Color RgbNaColor(int rgb) /// <summary>Barva do DXF <b>ve stejném druhu, v jakém přišla ze zdroje</b>: indexovaná zůstane
=> new((byte)((rgb >> 16) & 0xFF), (byte)((rgb >> 8) & 0xFF), (byte)(rgb & 0xFF)); /// čí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> /// <summary>Standardní tloušťky čar (1/100 mm) dle DXF; vstupní mm se přichytí k nejbližší.</summary>
private static readonly int[] StdTloustky = private static readonly int[] StdTloustky =
+52 -7
View File
@@ -8,12 +8,18 @@ public enum VAlign { Baseline, Bottom, Middle, Top }
/// <summary> /// <summary>
/// Vzhled písma přenesený z DXF textového stylu (ROZPISKY.md §3): rodina fontu + řez, /// 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 / /// <paramref name="ObliqueDeg"/> = sklon znaků ve stupních (DXF oblique angle, group 51 entity /
/// group 50 stylu); kladný = naklonění doprava, 0 = vzpřímeně. /// 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> /// </summary>
public readonly record struct TextStyle(string? FontFamily = null, bool Bold = false, bool Italic = false, 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í /// <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> /// 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)); 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 → /// <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> /// 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); public readonly record struct Contour(IReadOnlyList<Edge> Edges, bool Closed);
@@ -84,10 +126,12 @@ public static class ContourGeometry
/// </summary> /// </summary>
public interface IProfileRenderer public interface IProfileRenderer
{ {
/// <summary>Nastaví aktuální hladinu/styl kresby. <paramref name="rgb"/> = barva 0xRRGGBB /// <summary>Nastaví aktuální stav kresby hladinu a to, čím ji entita přebíjí
/// (rozbalená z DXF, ByLayer i index/true-color), tloušťka v mm. Bílá se kreslí černě /// (<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> /// (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 Line(double x1, double y1, double x2, double y2);
void Polyline(IReadOnlyList<(double X, double Y)> pts, bool closed = false); void Polyline(IReadOnlyList<(double X, double Y)> pts, bool closed = false);
void Circle(double cx, double cy, double r); 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 /// <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 /// 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> /// DXF HATCH např. loga mají hladké okraje v každém přiblížení i v PDF.
void Fill(IReadOnlyList<Contour> loops); /// <paramref name="vypln"/> říká, čím se plocha vyplní; <c>default</c> = plná.</summary>
void Fill(IReadOnlyList<Contour> loops, Vypln vypln = default);
} }
+79
View File
@@ -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 1255).</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);
}
+18 -4
View File
@@ -66,7 +66,8 @@ public sealed class SkiaProfileRenderer : IProfileRenderer, IDisposable
private SKTypeface Typeface(TextStyle style) private SKTypeface Typeface(TextStyle style)
{ {
// 1) Přesný font ze souboru, na který odkazuje DXF styl (.ttf v systémové složce Fonts). // 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); var fileKey = ("\0file:" + style.FontFile, 0, false);
if (_typefaces.TryGetValue(fileKey, out var tff)) return tff; 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); 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); 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á 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); var c = new SKColor(r, g, b);
@@ -259,9 +261,21 @@ public sealed class SkiaProfileRenderer : IProfileRenderer, IDisposable
return lines; return lines;
} }
public void Fill(IReadOnlyList<Contour> loops) public void Fill(IReadOnlyList<Contour> loops, Vypln vypln = default)
{ {
if (loops.Count == 0) return; 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 }; using var path = new SKPath { FillType = SKPathFillType.EvenOdd };
bool any = false; bool any = false;
foreach (var loop in loops) foreach (var loop in loops)
+143 -25
View File
@@ -1,53 +1,171 @@
namespace Rozpisky.Rendering; namespace Rozpisky.Rendering;
/// <summary> /// <summary>
/// Dekorátor <see cref="IProfileRenderer"/>, který na všechny souřadnice aplikuje /// Dekorátor <see cref="IProfileRenderer"/>, který na souřadnice aplikuje afinní transformaci
/// uniformní měřítko + posun: <c>X' = Ox + S·X</c>, <c>Y' = Oy + S·Y</c>. Slouží k umístění /// <c>X' = x + C·y + Tx</c>, <c>Y' = B·x + D·y + Ty</c>.
/// externího DXF fragmentu (logo / orientační schéma) do vymezeného rámečku rozpisky /// <para>Dvě použití: umístění externího DXF fragmentu (logo / orientační schéma) do rámečku
/// fragment se nakreslí ve svých souřadnicích a tato vrstva ho přemapuje do cílového obdélníku. /// rozpisky tam stačí měřítko a posun (<see cref="TransformRenderer(IProfileRenderer,double,double,double)"/>)
/// Výška textu, poloměr a tloušťka čar se škálují <c>·S</c>; hladina/barva propadá beze změny. /// 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> /// </summary>
public sealed class TransformRenderer : IProfileRenderer public sealed class TransformRenderer : IProfileRenderer
{ {
private readonly IProfileRenderer _inner; 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) 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; _inner = inner;
_s = scale; _a = a; _b = b; _c = c; _d = d; _tx = tx; _ty = ty;
_ox = offsetX;
_oy = offsetY; 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; /// <summary>Transformace vloženého bloku: entity bloku jsou zapsané vůči jeho vztažnému bodu,
private double Y(double y) => _oy + _s * y; /// 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) private double X(double x, double y) => _tx + _a * x + _c * y;
=> _inner.Layer(name, rgb, lineWeightMm * _s); 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) 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) 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) 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 public void Stroke(Contour c)
/// uniformní scale + posun, bez rotace/zrcadlení).</summary> {
private Edge T(Edge e) => e.Kind == EdgeKind.Line if (_zachovavaOblouky) { _inner.Stroke(new Contour(c.Edges.Select(T).ToList(), c.Closed)); return; }
? Edge.Line(X(e.SX), Y(e.SY), X(e.EX), Y(e.EY)) _inner.Polyline(Zplosti(c), c.Closed);
: 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); }
/// <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, public void Text(double x, double y, string text, double heightMm,
HAlign h = HAlign.Left, VAlign v = VAlign.Baseline, double rotationDeg = 0, HAlign h = HAlign.Left, VAlign v = VAlign.Baseline, double rotationDeg = 0,
TextStyle style = default, double widthFactor = 1, double maxWidthMm = 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) public void Fill(IReadOnlyList<Contour> loops, Vypln vypln = default)
=> _inner.Fill(loops.Select(c => new Contour(c.Edges.Select(T).ToList(), c.Closed)).ToList()); => _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;
}
} }
+49 -13
View File
@@ -215,20 +215,14 @@ public sealed class MainViewModel : ObservableObject
public IReadOnlyDictionary<string, string> ResolveLoga(Priloha r) public IReadOnlyDictionary<string, string> ResolveLoga(Priloha r)
=> LogaResolver.Resolve(Projekt, r, Ciselniky, RzpSlozka); => LogaResolver.Resolve(Projekt, r, Ciselniky, RzpSlozka);
/// <summary>Jména DXF souborů ve složce <c>loga\</c> vedle .exe (prázdná položka = „žádné") /// <summary>Jména DXF log z obou složek <c>loga\</c> uživatelské i dodávané (prázdná položka
/// nabídka pro výběr log financujících institucí v Údajích projektu.</summary> /// = „žádné") nabídka pro výběr log financujících institucí v Údajích projektu.</summary>
public IReadOnlyList<string> DostupnaLoga public IReadOnlyList<string> DostupnaLoga
{ {
get get
{ {
var list = new List<string> { "" }; var list = new List<string> { "" };
try list.AddRange(LogaResolver.DostupnaLoga());
{
if (Directory.Exists(LogaResolver.LogaDir))
list.AddRange(Directory.GetFiles(LogaResolver.LogaDir, "*.dxf")
.Select(Path.GetFileName)!);
}
catch { /* nedostupná složka → jen prázdná položka */ }
return list; return list;
} }
} }
@@ -311,6 +305,9 @@ public sealed class MainViewModel : ObservableObject
public ICommand ExportSeznamuCmd { get; } public ICommand ExportSeznamuCmd { get; }
public ICommand OtevritNapoveduCmd { get; } public ICommand OtevritNapoveduCmd { get; }
public ICommand OtevritManualCmd { 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 ICommand OProgramuCmd { get; }
public MainViewModel() public MainViewModel()
@@ -407,6 +404,7 @@ public sealed class MainViewModel : ObservableObject
ExportSeznamuCmd = new RelayCommand(ExportujSeznamDoXlsx, () => Prilohy.Count > 0); ExportSeznamuCmd = new RelayCommand(ExportujSeznamDoXlsx, () => Prilohy.Count > 0);
OtevritNapoveduCmd = new RelayCommand(OtevritNapovedu); OtevritNapoveduCmd = new RelayCommand(OtevritNapovedu);
OtevritManualCmd = new RelayCommand(OtevritManual); OtevritManualCmd = new RelayCommand(OtevritManual);
OtevritSlozkuLogCmd = new RelayCommand(OtevritSlozkuLog);
OProgramuCmd = new RelayCommand(OProgramu); OProgramuCmd = new RelayCommand(OProgramu);
SledujZmeny(); SledujZmeny();
@@ -836,6 +834,7 @@ public sealed class MainViewModel : ObservableObject
_exportZrusen = false; _exportZrusen = false;
int hotovo = 0, bezJmena = 0, n = prilohy.Count, index = 0; int hotovo = 0, bezJmena = 0, n = prilohy.Count, index = 0;
var chyby = new List<string>(); var chyby = new List<string>();
var pouziteLoga = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
try try
{ {
foreach (var p in prilohy) foreach (var p in prilohy)
@@ -855,7 +854,9 @@ public sealed class MainViewModel : ObservableObject
try 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++; hotovo++;
} }
catch (Exception ex) catch (Exception ex)
@@ -870,7 +871,8 @@ public sealed class MainViewModel : ObservableObject
StavovaZprava = "Připraveno"; 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> /// <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)); int cols = Math.Min(DxfSloupcu, Math.Max(1, n));
var dxf = new DxfProfileRenderer(); var dxf = new DxfProfileRenderer();
var chyby = new List<string>(); var chyby = new List<string>();
var pouziteLoga = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
try try
{ {
for (int i = 0; i < n; i++) 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) double oy = -row * (cell.H + DxfMezera); // další řádky pod sebe (osa Y nahoru)
try try
{ {
var loga = ResolveLoga(prilohy[i]);
pouziteLoga.UnionWith(loga.Values);
var draw = renderer.BuildDraw(Projekt, prilohy[i], Revize, Ciselniky, vyplnene: true, 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 // 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 // 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. // 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"; StavovaZprava = "Připraveno";
return; 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 DxfWriter.Write(cesta, dxf.Document, binary: true); // binární DXF = výrazně menší soubor
} }
catch (Exception ex) catch (Exception ex)
@@ -973,6 +979,7 @@ public sealed class MainViewModel : ObservableObject
sb.AppendLine($"Chyby: {chyby.Count}"); sb.AppendLine($"Chyby: {chyby.Count}");
foreach (var ch in chyby) sb.AppendLine($" • {ch}"); foreach (var ch in chyby) sb.AppendLine($" • {ch}");
} }
PripojNepodporovane(sb, RozpiskaRenderer.NepodporovanePrvky(pouziteLoga));
sb.AppendLine(); sb.AppendLine();
sb.Append($"Soubor: {cesta}"); sb.Append($"Soubor: {cesta}");
MessageBox.Show(sb.ToString(), "Export do DXF dokončen", MessageBoxButton.OK, 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> /// <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(); var sb = new System.Text.StringBuilder();
sb.AppendLine($"Vyexportováno do PDF: {hotovo}"); sb.AppendLine($"Vyexportováno do PDF: {hotovo}");
@@ -1212,6 +1220,7 @@ public sealed class MainViewModel : ObservableObject
sb.AppendLine($"Chyby: {chyby.Count}"); sb.AppendLine($"Chyby: {chyby.Count}");
foreach (var ch in chyby) sb.AppendLine($" • {ch}"); foreach (var ch in chyby) sb.AppendLine($" • {ch}");
} }
PripojNepodporovane(sb, nepodporovane);
sb.AppendLine(); sb.AppendLine();
sb.Append($"Složka: {cilRozpisky}"); sb.Append($"Složka: {cilRozpisky}");
@@ -1219,6 +1228,16 @@ public sealed class MainViewModel : ObservableObject
chyby.Count > 0 ? MessageBoxImage.Warning : MessageBoxImage.Information); 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 /// <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> /// aktuálního .rzp, uloží se relativně (jen jméno), jinak absolutní cestou.</summary>
private void VyberSchema() private void VyberSchema()
@@ -1399,4 +1418,21 @@ public sealed class MainViewModel : ObservableObject
"Nápověda", MessageBoxButton.OK, MessageBoxImage.Error); "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);
}
}
} }
+10 -1
View File
@@ -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="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 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"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Kořenová cesta exportu" Style="{StaticResource FieldLabel}"/> <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}"/> <TextBlock Grid.Row="2" Grid.Column="0" Text="DXF šablona" Style="{StaticResource FieldLabel}"/>
<TextBox Grid.Row="2" Grid.Column="1" Margin="{StaticResource Inp}" <TextBox Grid.Row="2" Grid.Column="1" Margin="{StaticResource Inp}"
Text="{Binding Nastaveni.SablonaDxf, UpdateSourceTrigger=PropertyChanged}"/> 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> </Grid>
</GroupBox> </GroupBox>
+2 -2
View File
@@ -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="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -146,7 +146,7 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="5" Style="{StaticResource Text.Caption}" Margin="0,0,0,6" <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}"/> <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}" <ComboBox Grid.Row="1" Grid.Column="1" Margin="{StaticResource Inp}"
+15
View File
@@ -147,6 +147,15 @@ Na revize se odkazují řádkové revizní sloupce přílohy (R1R4) a revizn
> mu je nesmí přepsat** zároveň smí nová verze dodávaný číselník libovolně měnit. > 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); > Klíč položky: `Nazev` (organizace, firmy, kraje), `Hodnota` (jména, verze), `Oznaceni` (části);
> porovnává se bez ohledu na velikost písmen. > 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íc­polový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 ### 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)). 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é - 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é. 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
View File
@@ -32,8 +32,13 @@ zvláštní `PlatformTarget` se nenastavuje.
### Zápis DXF co je potřeba vědět ### Zápis DXF co je potřeba vědět
- **Tloušťka čáry** = `Layer.LineWeight` (enum, hodnota v 1/100 mm standardní hodnoty: - **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`). `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 - **Diakritika** se do DXF ukládá jako Unicode escape `\U+XXXX` (např. `š``\U+0161`) AutoCAD
i ostatní čtečky to čtou správně. 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 - 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/ Rozpisky/
├─ Rendering/ ← kreslicí vrstva (nezávislá na cíli) ├─ 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) │ ├─ SkiaProfileRenderer.cs ← výstup přes SkiaSharp (obrazovka / PNG / PDF)
│ ├─ DxfProfileRenderer.cs ← výstup do DXF (ACadSharp) │ ├─ DxfProfileRenderer.cs ← výstup do DXF (ACadSharp)
│ ├─ BoundsRenderer.cs ← spočítá bbox kresby (fit / umístění na A4) │ ├─ 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 0255) → RGB
├─ Dxf/ ← čtení šablony, mapování hodnot, kódy ├─ Dxf/ ← čtení šablony, mapování hodnot, kódy
│ ├─ DxfTemplate.cs ← načte DXF šablonu a přehraje ji do IProfileRenderer │ ├─ 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) │ ├─ 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) │ ├─ 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) │ ├─ 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 | | 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 | | 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í | | 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í 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 HAlign { Left, Center, Right }
public enum VAlign { Baseline, Bottom, Middle, Top } public enum VAlign { Baseline, Bottom, Middle, Top }
public readonly record struct TextStyle(string? FontFamily = null, bool Bold = false, public enum BarvaDruh { ByLayer, ByBlock, Index, True }
bool Italic = false, string? FontFile = null); 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 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 Line(double x1, double y1, double x2, double y2);
void Polyline(IReadOnlyList<(double X, double Y)> pts, bool closed = false); void Polyline(IReadOnlyList<(double X, double Y)> pts, bool closed = false);
void Circle(double cx, double cy, double r); 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, void Text(double x, double y, string text, double heightMm,
HAlign h = HAlign.Left, VAlign v = VAlign.Baseline, double rotationDeg = 0, HAlign h = HAlign.Left, VAlign v = VAlign.Baseline, double rotationDeg = 0,
TextStyle style = default, double widthFactor = 1, double maxWidthMm = 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í čer **Proč `KresliciStav` a ne `int rgb`.** Šablona i všech 9 log jsou zapsané výhrad
(bílá na bílém papíře by zmizela). **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 10249 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í - `widthFactor` = horizontální měřítko písma (DXF width factor). `maxWidthMm` = šířka pro zalomení
víceřádkového textu (0 = jen explicitní zlomy). 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: Implementace:
- **`SkiaProfileRenderer`** kreslí na `SKCanvas`. Dostane funkci `Map(mmX, mmY) → bod plátna` - **`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. 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`. Je `IDisposable` (uvolní typefaces) používat přes `using`.
- **`DxfProfileRenderer`** pro každou hladinu založí `Layer` (barva + tloušťka), entity přidává do - **`DxfProfileRenderer`** staví ACadSharp entity do `Document`; hladiny, textové styly i barvy
`Document`. Výsledek zapíše `DxfWriter.Write(path, renderer.Document, binary: true)`. 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). - **`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ží - **`TransformRenderer`** dekorátor s afinní transformací: měřítko + posun (umístění loga do
k umístění vkládaného DXF fragmentu (logo / schéma) do vymezeného rámečku. 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. 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 - **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`. .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 - **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. (Excel, prohlížeč PDF), a nabídne Pokračovat/Přerušit.