diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 00000000..5bc677c2 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,59 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "website-ru-3001", + "runtimeExecutable": "npx", + "runtimeArgs": [ + "docusaurus", + "start", + "--locale", + "ru", + "--port", + "3001", + "--no-open" + ], + "cwd": "Website", + "port": 3001 + }, + { + "name": "website-dev", + "runtimeExecutable": "npx", + "runtimeArgs": [ + "docusaurus", + "start", + "--port", + "3100", + "--no-open" + ], + "cwd": "Website", + "port": 3100 + }, + { + "name": "website-dev-ru", + "runtimeExecutable": "npx", + "runtimeArgs": [ + "docusaurus", + "start", + "--locale", + "ru", + "--port", + "3101", + "--no-open" + ], + "cwd": "Website", + "port": 3101 + }, + { + "name": "website-serve-all", + "runtimeExecutable": "sh", + "runtimeArgs": [ + "-c", + "npm run build && npx docusaurus serve --port 3001 --no-open" + ], + "cwd": "Website", + "_note": "Only when Website/scripts/serve-all.sh is unusable: this ties the shared 3001 server to one session.", + "port": 3001 + } + ] +} diff --git a/.claude/skills/docs-site/SKILL.md b/.claude/skills/docs-site/SKILL.md index 15bdcd33..4afaeb30 100644 --- a/.claude/skills/docs-site/SKILL.md +++ b/.claude/skills/docs-site/SKILL.md @@ -1,91 +1,183 @@ --- name: docs-site -description: How Aspid.FastTools documentation is authored and published — Markdown inside the UPM package (`Documentation/`, `Documentation/ru/`, each sample's `README.md` / `TUTORIAL.md`) read by GitHub, Unity and the Docusaurus site in `Website/`, deployed to GitHub Pages. Use when adding or editing any documentation page, translation, sample README, or the site itself. +description: How Aspid.FastTools documentation is authored and published — Markdown inside the UPM package (`Documentation/`, `Documentation/ru/`, each sample's `Documentation/`) plus the root `CHANGELOG*.md`, read by GitHub, Unity and the Docusaurus site in `Website/`, deployed to GitHub Pages. Use when adding or editing any documentation page, translation, sample README, image, or the site itself. user-invocable: false --- # Documentation site -One source of truth: Markdown files inside the UPM package. The same file is read by GitHub, by Unity -(as a `TextAsset` in the Inspector) and by the Docusaurus site in `Website/`. Nothing is copied by hand -and the root `README.md` is only an overview with links. Write GitHub Flavored Markdown; the site adapts -to it, never the other way round. +One source of truth: Markdown inside the UPM package. The same file is read by GitHub, by Unity (as a +`TextAsset` in the Inspector) and by the Docusaurus site in `Website/`. Nothing is copied by hand — the root +`README.md`, the tutorials tree, the i18n tree, the changelog page and the API reference are all generated. +Write GitHub Flavored Markdown; the site adapts to it, never the other way round. + +Package-relative paths below are rooted at `Aspid.FastTools/Packages/tech.aspid.fasttools/`. ## Layout -| What | Where | Site route | +| What | Source | Site route | |---|---|---| -| Main docs | `Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/NN-*.md` | `/docs/` (`02-serializable-types.md` → `/docs/serializable-types`) | | Introduction | `Documentation/README.md` | `/docs` | -| Tutorials | `Samples~//README.md` and `TUTORIAL.md` | `/tutorials/`, `/tutorials//tutorial` (`SerializeReferences` → `serialize-references`; an optional `NN. ` folder prefix orders and is stripped) | -| Translations | `Documentation/ru/**` (same names), `Samples~//README.ru.md`, `TUTORIAL.ru.md` | `/ru/...` | -| Images | `Documentation/Images/` | referenced relatively | -| Site config | `Website/docusaurus.config.js`, `sidebars.js`, `sidebarsTutorials.js` | | +| Main docs | `Documentation/NN-*.md` | `/docs/` (`02-serializable-types.md` → `/docs/serializable-types`) | +| Samples | `Samples~//Documentation/README.md` | `/tutorials/` (`SerializeReferences` → `serialize-references`; an optional `NN. ` folder prefix orders and is stripped) | +| Samples overview | `Website/src/samples/index.mdx`, `index.ru.mdx` → `` | `/tutorials` | +| Changelog | root `CHANGELOG.md`, `CHANGELOG.ru.md` | `/changelog` | +| API reference | generated into `Website/api/` by DocFX (committed) | `/api` | +| Translations | `Documentation/ru/**` (same names), `Samples~//Documentation/README.ru.md` | `/ru/...` | +| Images | `Documentation/Images/`, `Samples~//Documentation/Images/` | referenced relatively | +| Root README | generated from `Documentation/README.md` (committed) | — | +| Site config | `Website/docusaurus.config.js`, `sidebars.js`, `sidebarsTutorials.js`, `sidebarsApi.js` | | | CI | `.github/workflows/docs.yml` → GitHub Pages `https://vpdpersonal.github.io/Aspid.FastTools/` | | -The site folder is `Website/`, not `Docs/`: the repo already has `docs/` (internal working documents) and +The site folder is `Website/`, not `Docs/`: the repo already has `docs/` (internal working documents, plus +`docs/images/` which hosts the README banner GIF that GitHub serves over `raw.githubusercontent.com`), and macOS treats the two names as one directory. -Two docs plugin instances: the default one (`docs`) points at `Documentation/` (translation folders and -`SUMMARY.md` excluded), the second (`id: 'tutorials'`) points at `Samples~/` with -`include: ['*/README.md', '*/TUTORIAL.md']`. Scripts, scenes and `.meta` files are never touched. +**Four docs plugin instances**, all in `docusaurus.config.js`: -`Website/scripts/sync-i18n.mjs` (run by `prestart`/`prebuild`) copies `Documentation//`, every -`*..md` in a sample root, and `Documentation/Images/` into the `Website/i18n/` layout Docusaurus expects. -`Website/i18n/` is a build artifact and is gitignored; never edit it by hand. +- `docs` — reads the package `Documentation/` in place; locale folders are excluded through `LOCALES`. +- `tutorials` — reads the generated `Website/tutorials/` tree (`include: ['index.mdx', '*/README.md']`). + One page per sample; the overview page comes from `src/samples/index.mdx`. +- `changelog` — reads the generated `Website/changelog/`, whose sidebar is built from the `## [version]` + headings (each gets a `{#v…}` anchor). +- `api` — reads the committed `Website/api/`. -`SUMMARY.md` is the GitHub table of contents; the site sidebar is autogenerated from the folder. +Generated and gitignored: `Website/tutorials/`, `Website/i18n/`, `Website/changelog/`, `Website/build/`, +`Website/docfx/projects/`. Never edit them by hand. Generated **and committed**: the root `README.md` and +`Website/api/`. ## Writing rules (so all three renderers agree) -- **No front matter.** Unity and GitHub would show it as text. Title comes from the first `# H1`, - slug and order come from the file name (`NN-` prefix orders, is stripped from the route). +- **No front matter.** Unity and GitHub would show it as text. Title comes from the first `# H1`, slug and + order from the file name (`NN-` prefix orders, is stripped from the route). +- **One `# H1` per file.** Use `##` in the body. Exception: the introduction (`Documentation/README.md` and + its translations) starts with the banner `` and the status badges, without an H1. `parseFrontMatter` + in `docusaurus.config.js` recognises that page by the banner's file name and supplies the title, + description and `hide_title` — do not rename `aspid_fasttools_readme_banner.gif`. - **Admonitions**: GitHub style only — `> [!NOTE]`, `TIP`, `IMPORTANT`, `WARNING`, `CAUTION`. Never `:::note`. - **Links** are relative paths to the `.md` file: `[EnumValues](06-enum-values.md)`, from a sample - `[Selector](../../Documentation/03-serialize-reference-selector.md)`, from a doc - `[Types sample](../Samples~/Types/README.md)`. GitHub follows them as files; links that cross between - the two plugin instances are rewritten to site routes by `Website/src/remark/crossInstanceLinks.js`. + `[Selector](../../../Documentation/03-serialize-reference-selector.md)`, from a doc + `[Types sample](../Samples~/Types/Documentation/README.md)`. GitHub follows them as files; links that cross + between plugin instances are rewritten to site routes by `Website/src/remark/crossInstanceLinks.js`. Never link by site URL. -- **One `# H1` per file.** Use `##` in the body. -- **Images** live in `Documentation/Images/` and are referenced relatively (`Images/x.png` from a doc, - `../Images/x.png` from `ru/`, `../../Documentation/Images/x.png` from a sample). -- **Every `.md` in the package needs a `.meta`** (`TextScriptImporter`) — Unity would otherwise generate one - in the consumer's project. Copy an existing one and give it a fresh GUID. +- **Before/after comparisons**: a two-column table whose cells are `
` stays portable + on GitHub and becomes real highlighted code blocks on the site (`src/remark/introBanner.js`). +- **Every `.md` and every image in the package needs a `.meta`** (`TextScriptImporter` for Markdown) — Unity + would otherwise generate one in the consumer's project. Copy an existing one and give it a fresh GUID. - The package is English. A translation is a sibling file: `Documentation/ru/06-enum-values.md`, `README.ru.md` next to `README.md`. Missing pages fall back to English. A translated file links translated - targets (`../../Samples~/Types/README.ru.md`) so GitHub stays in the same language; the site drops the - locale segment itself. -- Adding a language: create `Documentation//` and `*..md` files and add the locale to - `LOCALES` in `docusaurus.config.js`. Nothing else: `sync-i18n.mjs` discovers locale folders by name, and - `LOCALES` is what keeps them out of the English `docs` instance. + targets (`../../Samples~/Types/Documentation/README.ru.md`) so GitHub stays in the same language; the site + drops the locale segment itself. +- Adding a language: create `Documentation//` and `*..md` files, add `Website/translations//` + for the interface strings, and add the locale to `LOCALES` in `docusaurus.config.js`. Nothing else: + `sync-i18n.mjs` discovers locale folders by name, and `LOCALES` is what keeps them out of the English + `docs` instance. + +### Images + +- Main docs use `Documentation/Images/`; each sample keeps its own in `Samples~//Documentation/Images/` + and references them as `Images/x.png`. A main doc may point at a sample image by path + (`../Samples~/EnumValues/Documentation/Images/demo.gif`); `sync-i18n.mjs` mirrors those folders for i18n. +- **Every capture needs a light-theme sibling**: `x.png` plus `x-light.png` in the same folder. + `src/remark/themedImages.js` swaps them per theme; without the sibling, light mode shows the dark capture. +- **Editor captures are framed automatically.** In `/docs` and `/tutorials` an image renders inside the + window frame (`doc-image-panel`, `src/theme/MDXComponents/Img`). The exception is `demo`/`scene` + (`.gif`/`.png`) on a *tutorial* page, which keeps the bare scene look; the same file on a doc page is framed. + So name inspector captures anything but `demo`/`scene`, and name scene footage exactly that. +- `.sample-scene` (the background-recolouring filter) is applied by `themedImages.js` only to `demo`/`scene` + files inside a **hardcoded list of sample folders** — a new sample must be added to that regex. +- A paragraph that repeats the image's alt text right below it becomes the caption (`doc-media-caption`). +- Click or Enter opens the image in a modal (Esc closes). Unframed images are capped at 640×520; + framed and `.sample-scene` media fill the article. +- Status badges (`Images/status-badge-*.svg`) are links, not captures: they keep their size and do not zoom. + +## Writing a feature page (docs/) + +Rules the user confirmed while reworking `08-serialized-property-extensions.md` and `09-editor-helpers.md`; apply them +to every main doc page, always to the English file and its `ru/` twin together. + +- **Lead paragraph = what the feature does, not a table of contents.** One or two sentences a reader understands + without knowing the API, ideally with the visible result (`FireAbility` → "Fire Ability"). No "for X, Y and Z" + enumerations of sections, no "use it for titles and lists" purpose sentence, no abstract wording + ("resolves the property back to its owner"). +- **One concrete example type per page**, declared in the quick start ("The examples on this page work with the + `AbilityBook` component:") and reused by every section. Extend that type rather than inventing a second one. +- **Verify every claim against the source** (`Editor/Scripts/...`) before writing it; drop anything the code does + not back (e.g. the "inherited attribute" note was removed from `GetDisplayName`). +- **Results go in tables**: property × method result tables and Unity-API-vs-FastTools before/after tables replace + runs of small code blocks. Long method lists (setters) become a grouped table, not a comma list. +- **Say each fact once.** No repeat between a table's cell comments and the paragraph under it, and no repeat + between quick start and a later section (`AndApply` is explained once). +- **Do not state what the context already implies** (no editor-only note under "in its custom `Editor`"). + Never stack two admonitions. A pitfall that silently loses data gets a `> [!WARNING]` (boxed struct copy). +- **Sample reference is minimal**: a closing `## Package sample` / `## Пример в пакете` with one sentence, the + link to the sample README and, when the sample has one, its `demo.gif` with the caption paragraph — no + "how to open" steps or experiments, those live on the sample's own page. ## Adding a main doc page -Drop `NN-name.md` into `Documentation/`, add its row to `Documentation/README.md` (feature table) and to -`SUMMARY.md`, add the `.meta`, optionally the translation at `Documentation/ru/NN-name.md`. Nothing to change -in `Website/`. Also update the feature table in the root `README.md`. +Drop `NN-name.md` into `Documentation/`, add its section to `Documentation/README.md` (and `ru/README.md`), +add the `.meta`, optionally the translation at `Documentation/ru/NN-name.md`, and add its id to the right +group in `Website/sidebars.js` (Serialization / Editor & tooling). Run `npm --prefix Website run sync-readme` +to refresh the root `README.md`. ## Adding a sample -1. Create `Samples~//README.md` (+ `TUTORIAL.md` if there is a guided scene), with `.meta` files. -2. Add a category with `/readme` and `/tutorial` to `Website/sidebarsTutorials.js`; drop the - `items` entry if there is no `TUTORIAL.md`. -3. Register the sample in `package.json` → `samples`, and add its row to `01-getting-started.md` (EN and `ru/`). +1. `Samples~//Documentation/README.md` (+ `README.ru.md`), with `.meta` files. Images go in that + sample's `Documentation/Images/`, each with its `-light` sibling. +2. `Website/sidebarsTutorials.js`: add `{ type: 'doc', id: '/readme', label: '' }`. +3. `Website/src/components/SamplesGallery/index.js`: add an entry (id = slug, feature name, en/ru title and + description) and put its preview at `Website/static/img/samples/.png` + `-light.png`. +4. If the sample ships `demo`/`scene` captures, add its folder to the sample regex in + `Website/src/remark/themedImages.js`. +5. List it in the samples overview (`Samples~/README.md`, `README.ru.md`) and register it in the package + `package.json` → `samples`. ## Local run / check +**Shared server (default).** The user works on the English and Russian versions at the same time, and other +agents work on the site in parallel, so everything is checked on **one shared production build** served on +port 3001 — never on per-agent dev servers: + +```bash +Website/scripts/serve-all.sh # kill the old server, `npm run build` (en + ru), serve detached on 3001 +``` + ```bash -cd Website -npm ci -npm start # http://localhost:3000/Aspid.FastTools/ (prestart syncs translations) -npm run start:ru # Russian locale (dev server serves one locale at a time) -npm run build # what CI runs; fails on broken links +Website/scripts/serve-all.sh --stop ``` +- English: `http://localhost:3001/Aspid.FastTools/`, Russian: `http://localhost:3001/Aspid.FastTools/ru/`. +- The server is detached (`nohup`, log in `Website/.serve-all.log`), so it outlives the session that started it + and every agent and the user see the same site. Do not start it through `preview_start` — that ties it to one + session. +- A static build does **not** pick up edits: after **every** change you want to verify (Markdown, config, remark + plugins, CSS, sidebars), rerun `serve-all.sh` yourself and only then check in the browser. Never ask the user + to restart it. The rebuild takes about a minute. +- If port 3001 is already answering when you start, another agent's build is up — rerun the script anyway after + your edits; it replaces the server safely. Do not run `npm run build` or a dev server from `Website/` while the + script is building (they share `.docusaurus/`, `build/` and `i18n/`). + +Dev servers serve one locale at a time and are only for quick hot-reload iteration on a single page — they +don't reload config or remark plugins, and the user does not look at them: `npm start` / `npm run start:ru`, or +`website-dev` / `website-dev-ru` in `.claude/launch.json` (3100/3101). The `website-ru-3001` and +`website-serve-all` entries in that file both occupy port 3001 and would replace the shared build with a +session-bound server — do not launch them. + `onBrokenLinks` and `onBrokenMarkdownLinks` are `throw`: a bad relative link breaks the build on purpose (`onBrokenAnchors` only warns — check the log for `#anchor` typos). -Do not run `npm run build` while a dev server is running from the same folder — they share `.docusaurus/` -and `i18n/`. + +## Generated content + +- `npm --prefix Website run sync-readme` regenerates the root `README.md` from `Documentation/README.md`, + rebasing file links to the repository root. Never edit the root README by hand. `prestart`/`prebuild` refresh + it automatically and CI runs `check-readme` before building to reject a stale copy. +- `Website/scripts/sync-i18n.mjs` (also run by `prestart`/`prebuild`) builds `Website/tutorials/`, + `Website/changelog/` and `Website/i18n/` from the package: English sample READMEs and their images, + `Documentation//`, every sample-local `*..md`, the root changelogs and + `Website/translations//`. Scripts, scenes and `.meta` files are never copied. Because the copies are + untracked, each page's "Last updated" date is stamped from the **source file's last commit** — an + uncommitted page shows no date. ## Versioning @@ -101,10 +193,15 @@ installation, which CI does not have. ```bash dotnet tool install -g docfx # once; the tool lands in ~/.dotnet/tools — make sure it is on PATH +``` + +```bash cd Website && npm run api # regenerate after public API or XML doc changes ``` -`npm run api` runs three steps (`Website/scripts/docfx-*.mjs`, `Website/docfx/docfx.json`): +`npm run api` deletes `Website/api/` and runs three steps (`Website/scripts/docfx-*.mjs`, +`Website/docfx/docfx.json`) — if DocFX fails midway, the directory stays empty and the site build breaks, so +regenerate or `git checkout Website/api` before building: 1. `docfx-projects.mjs` writes SDK-style projects for `Aspid.FastTools` and `Aspid.FastTools.Editor` under `Website/docfx/projects/` (gitignored, absolute paths). Sources come from each asmdef folder (so a stale @@ -120,10 +217,11 @@ cd Website && npm run api # regenerate after public API or XML doc namespace suffix in its label — Docusaurus derives one translation key per label and the `ru` build fails on duplicates. -The pages are served by the third docs plugin instance (`id: 'api'`). Never edit files in `Website/api/` by hand; -fix the XML comment or the postprocess script and regenerate. Translations are not generated; the `ru` locale -falls back to the English pages. The Math satellite assembly is not documented — it compiles only when -`com.unity.mathematics` is installed, which this project does not. +`Website/sidebarsApi.js` adapts the generated sidebar for display (drops the repeated `Aspid.FastTools.` +prefix, folds the `SetLabel` overloads). Never edit files in `Website/api/` by hand; fix the XML comment or the +postprocess script and regenerate. Translations are not generated; the `ru` locale falls back to the English +pages. The Math satellite assembly is not documented — it compiles only when `com.unity.mathematics` is +installed, which this project does not. ## Design @@ -135,12 +233,21 @@ redirects `/` to `/docs` — there is no landing page yet. `static/img/logo.png` and `favicon.png` are copies of the package icon `Editor/Resources/Icons/aspid_icon_medium_green_256x253.png`; re-copy them if the icon changes. -The article scrolls inside its own panel, not the window. Docusaurus' TOC highlight listens to `document` -scroll, so `src/clientModules/panelScroll.js` re-dispatches the panel's scroll events. If you change the layout -so the page scrolls normally again, drop that module; if you rename the scrolling container, update its selector. +The page uses normal document scrolling with sticky navigation. The borderless article has an opaque reading +surface (graphite in dark mode, warm linen in light mode). The fixed dot texture is painted on `html`, not the +viewport-height `body`, so it remains visible in the margins throughout long articles. Both navigation columns +share `--venom-navigation-width` (260px). Below 1400px the right TOC becomes an in-article disclosure, and +below 997px the navigation uses Docusaurus' mobile menu. On desktop (≥997px) the navbar is hidden and +`src/theme/DocSidebar/Desktop` wraps the sidebar into a full-height panel: pinned header (brand, section +switcher built from the navbar's left items, search), scrolling document list, pinned footer (GitHub, +language, theme). + +`src/plugins/search` builds a locale-specific index from Docusaurus' resolved document sources and permalinks. +`src/theme/SearchBar` loads it on demand, searches Docs/Samples/API/Changelog, and supports Cmd/Ctrl+K, arrow +keys, Enter and Esc. `node --test scripts/search.test.mjs` checks matching and Markdown extraction. ## Deploy -`.github/workflows/docs.yml` builds on every push to `main` touching `Website/`, `Documentation/` or a sample -`README*.md` / `TUTORIAL*.md`, and on PRs (build only). Pages source must be set to "GitHub Actions" once in the -repository settings. +`.github/workflows/docs.yml` builds on every push to `main` touching `Website/`, the package `Documentation/`, +a sample's `Documentation/`, the root `README.md` or `CHANGELOG*.md`, and on PRs (build only); it runs +`check-readme` before the build. Pages source must be set to "GitHub Actions" once in the repository settings. diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 46afca9a..f60c9bf1 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,7 +1,7 @@ name: Docs # Builds the Docusaurus site in Website/ and publishes it to GitHub Pages. -# Content comes from the package (Documentation/ and Samples~/*/README*.md, TUTORIAL*.md) and the root CHANGELOG*.md, +# Content comes from the package (Documentation/ and Samples~/*/Documentation/) and the root CHANGELOG*.md, # so any change there rebuilds the site. on: @@ -9,17 +9,17 @@ on: branches: [main] paths: - 'Website/**' + - 'README.md' - 'Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/**' - - 'Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/*/README*.md' - - 'Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/*/TUTORIAL*.md' + - 'Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/*/Documentation/**' - 'CHANGELOG*.md' - '.github/workflows/docs.yml' pull_request: paths: - 'Website/**' + - 'README.md' - 'Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/**' - - 'Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/*/README*.md' - - 'Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/*/TUTORIAL*.md' + - 'Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/*/Documentation/**' - 'CHANGELOG*.md' - '.github/workflows/docs.yml' workflow_dispatch: @@ -45,6 +45,8 @@ jobs: cache-dependency-path: Website/package-lock.json - run: npm ci working-directory: Website + - run: npm run check-readme + working-directory: Website - run: npm run build working-directory: Website - uses: actions/upload-pages-artifact@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d9050e16..70c07710 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,6 +84,18 @@ jobs: fi echo "package.json version matches: $PKG_VERSION" + - name: Validate documentation version + run: | + set -euo pipefail + VERSION="${{ steps.version.outputs.version }}" + for f in README.md "$PACKAGE_PATH/Documentation/README.md" "$PACKAGE_PATH/Documentation/ru/README.md" \ + "$PACKAGE_PATH/Documentation/Images/status-badge-preview.svg"; do + if ! grep -qF "$VERSION" "$f"; then + echo "::error file=$f::$f does not mention $VERSION. Run scripts/set-version.sh $VERSION." + exit 1 + fi + done + - name: Extract CHANGELOG section id: changelog run: | diff --git a/CLAUDE.md b/AGENTS.md similarity index 77% rename from CLAUDE.md rename to AGENTS.md index 923aff6b..604cda37 100644 --- a/CLAUDE.md +++ b/AGENTS.md @@ -12,4 +12,6 @@ Unity package `tech.aspid.fasttools` (`Aspid.FastTools/Packages/tech.aspid.fastt ## Not obvious - A change to generator or analyzer source reaches Unity **only** after `dotnet build -c Release` in that solution; - `dotnet test` (Debug) deliberately does not copy the DLL, so it is safe to run. \ No newline at end of file + `dotnet test` (Debug) deliberately does not copy the DLL, so it is safe to run. +- The version lives in `package.json`, the badge SVG and the install URLs of both READMEs; bump all of them with + `scripts/set-version.sh `, which the release workflow checks. diff --git a/Aspid.FastTools.Analyzers/Aspid.FastTools.Analyzers/Aspid.FastTools.Analyzers.Sample/Sample.cs b/Aspid.FastTools.Analyzers/Aspid.FastTools.Analyzers/Aspid.FastTools.Analyzers.Sample/Sample.cs deleted file mode 100644 index 769c6b77..00000000 --- a/Aspid.FastTools.Analyzers/Aspid.FastTools.Analyzers/Aspid.FastTools.Analyzers.Sample/Sample.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Demonstrates [TypeSelector] usages the analyzer accepts. The analyzer is referenced as an Analyzer, so building -// this project runs AFT0001–AFT0005 against the code below — it stays clean because every usage is valid. -// -// Minimal stand-ins for the real attributes (this sample references neither Unity nor the package). -namespace UnityEngine -{ - public class Object { } - // Unity declares SerializeReference without the "Attribute" suffix — the analyzer matches by that display name. - public sealed class SerializeReference : System.Attribute { } -} - -namespace Aspid.FastTools.Types -{ - [System.Flags] public enum TypeAllow { None = 0, Abstract = 1, Interface = 2, All = 3 } - - public sealed class TypeSelectorAttribute : System.Attribute - { - public TypeSelectorAttribute() { } - public TypeSelectorAttribute(System.Type type) { } - public TypeSelectorAttribute(params System.Type[] types) { } - public TypeAllow Allow { get; set; } - } -} - -namespace Aspid.FastTools.Analyzers.Sample -{ - using UnityEngine; - using Aspid.FastTools.Types; - using System.Collections.Generic; - - public interface IWeapon { } - public interface IMelee : IWeapon { } - - // Concrete implementations so AFT0005 does not fire — the picker has at least one candidate for IWeapon/IMelee. - public class Sword : IMelee { } - public class Bow : IWeapon { } - - public sealed class Loadout - { - // String type-name picker: Allow may opt in abstract/interface types because a Type is named, not instantiated. - [TypeSelector(typeof(IWeapon), Allow = TypeAllow.Interface)] - private string _weaponTypeName; - - // Managed reference: candidates default to the field's declared type. - [SerializeReference, TypeSelector] - private IWeapon _primary; - - // Managed reference narrowed by a base type assignable to the field type. - [SerializeReference, TypeSelector(typeof(IMelee))] - private IWeapon _sidearm; - - // Collections of managed references are supported too. - [SerializeReference, TypeSelector] - private List _stash; - } -} diff --git a/Aspid.FastTools/Assets/DevTests/Enums/Scripts/EnumValuesIMGUITest.cs b/Aspid.FastTools/Assets/DevTests/Enums/Scripts/EnumValuesIMGUITest.cs index 6175d7de..d8aee815 100644 --- a/Aspid.FastTools/Assets/DevTests/Enums/Scripts/EnumValuesIMGUITest.cs +++ b/Aspid.FastTools/Assets/DevTests/Enums/Scripts/EnumValuesIMGUITest.cs @@ -51,5 +51,11 @@ public struct Profile // [Flags] key handling. [SerializeField] private EnumValues _typedFlags; + + // Object-reference values: the row draws an object field rather than a plain value or a foldout. + [SerializeField] private EnumValues _typedObjectValues; + + // Tables inside a collection: the drawer is applied per element, one table per array entry. + [SerializeField] private EnumValues[] _typedArray; } } diff --git a/Aspid.FastTools/Assets/DevTests/Enums/Scripts/EnumValuesUIToolkitTest.cs b/Aspid.FastTools/Assets/DevTests/Enums/Scripts/EnumValuesUIToolkitTest.cs index 870b62b3..50f953f5 100644 --- a/Aspid.FastTools/Assets/DevTests/Enums/Scripts/EnumValuesUIToolkitTest.cs +++ b/Aspid.FastTools/Assets/DevTests/Enums/Scripts/EnumValuesUIToolkitTest.cs @@ -16,5 +16,9 @@ public sealed class EnumValuesUIToolkitTest : MonoBehaviour [SerializeField] private EnumValues _typedFoldout; [SerializeField] private EnumValues _typedFlags; + + [SerializeField] private EnumValues _typedObjectValues; + + [SerializeField] private EnumValues[] _typedArray; } } diff --git a/Aspid.FastTools/Assets/DevTests/README.md b/Aspid.FastTools/Assets/DevTests/README.md new file mode 100644 index 00000000..eaf4b895 --- /dev/null +++ b/Aspid.FastTools/Assets/DevTests/README.md @@ -0,0 +1,51 @@ +# DevTests + +Manual harnesses for `tech.aspid.fasttools`, kept in the development project only: nothing here is part +of the package or of its samples, and nothing here ships to users. + +The package's own `Tests/Editor` assembly covers the logic, and `Samples~` covers the ordinary, +user-facing flows in UI Toolkit. What is left for this folder is what neither can do: render a drawer +through the IMGUI path, and hold assets that are deliberately broken. + +## Types + +`TypesIMGUITest` and `TypesUIToolkitTest` carry the same fields — `SerializableType`, its generic and +`SerializableMonoScript` variants, `[TypeSelector]` on strings and arrays, every `TypeAllow` category, +`Required`, and a `nameof` member reference. `TypesIMGUITestEditor` forces the first one through IMGUI, +so `Prefabs/TypesDevTest.prefab` shows both renderings of one field list side by side. + +The prefab's `FastEnemy` child covers `ComponentTypeSelector`: `EnemyBaseEditor` is registered with +`editorForChildClasses: true`, so the inspector stays in IMGUI after the component swaps its own type. + +`Scripts/DocsMedia` holds the types that appear in the published screenshots of +`Documentation/02-serializable-types.md`. Their namespaces are neutral and game-like because picker +breadcrumbs are visible in the captures; changing a name or a group there invalidates an image. + +## SerializeReferences + +`SerializeReferencesIMGUITest` and `SerializeReferencesUIToolkitTest` cover the notice states on +`Prefabs/SerializeReferencesDevTest.prefab`: an unset `Required` reference, one instance shared by two +fields, a missing type with Fix and Smart Fix, and a missing element inside a list. + +`RequiredViolationsDevTest` leaves a managed reference and a type name unset on purpose, which is what +the Project References window groups under required violations and the Asset References window badges. + +This folder is listed under **Excluded folders** in the project's SerializeReference settings, so the +project-wide scan and the `sr_gate` command pass over everything here; the samples carry their own broken +assets for that. Remove the entry to see these fixtures in Project References, and put it back afterwards. + +`Scripts/Fixtures` backs three prefabs that exist as files rather than inspectors, because the audit +windows scan assets on disk: `WeaponPreset` (two ordinary references), `SharedWeaponPreset` (one +instance behind both fields) and `BrokenWeaponPreset` (a stored class name that resolves to nothing). + +## Enums + +`EnumValuesIMGUITest` and `EnumValuesUIToolkitTest` pair the same tables on +`Prefabs/EnumValuesDevTest.prefab`: the untyped variant with its type-picker row, single-line and +multi-field values, `[Flags]` keys, object-reference values, and tables inside an array. + +## CliCommands + +`sr_gate` exposes the SerializeReference gate scanner to `unity command`, so the scan returns JSON from +the running Editor instead of a batchmode relaunch. It is the only assembly here, because it needs the +package's internals. diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/01-getting-started.md.meta b/Aspid.FastTools/Assets/DevTests/README.md.meta similarity index 75% rename from Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/01-getting-started.md.meta rename to Aspid.FastTools/Assets/DevTests/README.md.meta index 5527fe9e..63d04d2d 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/01-getting-started.md.meta +++ b/Aspid.FastTools/Assets/DevTests/README.md.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: a1a8f7a8852c43b9b3c958b3f2be8227 +guid: e65b2560009e4d3d8ef00392585f41ba TextScriptImporter: externalObjects: {} userData: diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Prefabs/BrokenWeaponPreset.prefab b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Prefabs/BrokenWeaponPreset.prefab index d7776ce7..7bae620d 100644 --- a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Prefabs/BrokenWeaponPreset.prefab +++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Prefabs/BrokenWeaponPreset.prefab @@ -58,5 +58,5 @@ MonoBehaviour: - rid: 2339642446330462276 type: {class: Shotgun, ns: Game.Gear, asm: Assembly-CSharp} data: + _damage: 37 _pellets: 8 - _spread: 12 diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Prefabs/SerializeReferencesDevTest.prefab b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Prefabs/SerializeReferencesDevTest.prefab new file mode 100644 index 00000000..2ded9b04 --- /dev/null +++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Prefabs/SerializeReferencesDevTest.prefab @@ -0,0 +1,123 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &7412330891455062001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7412330891455062002} + - component: {fileID: 7412330891455062003} + - component: {fileID: 7412330891455062004} + m_Layer: 0 + m_Name: SerializeReferencesDevTest + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &7412330891455062002 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7412330891455062001} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &7412330891455062003 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7412330891455062001} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: b420f95844b84734b4a50fffdc0a01e3, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::Aspid.FastTools.DevTests.SerializeReferences.SerializeReferencesIMGUITest + _required: + rid: -2 + _sharedLeft: + rid: 7412330891455063000 + _sharedRight: + rid: 7412330891455063000 + _missing: + rid: 7412330891455063001 + _missingInList: + - rid: 7412330891455063002 + - rid: 7412330891455063003 + references: + version: 2 + RefIds: + - rid: 7412330891455063000 + type: {class: Pistol, ns: Game.Gear, asm: Assembly-CSharp} + data: + _damage: 12 + - rid: 7412330891455063001 + type: {class: Pistoll, ns: Game.Gear, asm: Assembly-CSharp} + data: + _damage: 37 + - rid: 7412330891455063002 + type: {class: Shotgun, ns: Game.Gear, asm: Assembly-CSharp} + data: + _damage: 20 + _pellets: 6 + - rid: 7412330891455063003 + type: {class: Shotgunn, ns: Game.Gear, asm: Assembly-CSharp} + data: + _damage: 44 + _pellets: 8 +--- !u!114 &7412330891455062004 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7412330891455062001} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 19ed43d7d91a47a7b0ed8f22852b914d, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::Aspid.FastTools.DevTests.SerializeReferences.SerializeReferencesUIToolkitTest + _required: + rid: -2 + _sharedLeft: + rid: 7412330891455064000 + _sharedRight: + rid: 7412330891455064000 + _missing: + rid: 7412330891455064001 + _missingInList: + - rid: 7412330891455064002 + - rid: 7412330891455064003 + references: + version: 2 + RefIds: + - rid: 7412330891455064000 + type: {class: Pistol, ns: Game.Gear, asm: Assembly-CSharp} + data: + _damage: 12 + - rid: 7412330891455064001 + type: {class: Pistoll, ns: Game.Gear, asm: Assembly-CSharp} + data: + _damage: 37 + - rid: 7412330891455064002 + type: {class: Shotgun, ns: Game.Gear, asm: Assembly-CSharp} + data: + _damage: 20 + _pellets: 6 + - rid: 7412330891455064003 + type: {class: Shotgunn, ns: Game.Gear, asm: Assembly-CSharp} + data: + _damage: 44 + _pellets: 8 diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Prefabs/SerializeReferencesDevTest.prefab.meta b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Prefabs/SerializeReferencesDevTest.prefab.meta new file mode 100644 index 00000000..60f14962 --- /dev/null +++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Prefabs/SerializeReferencesDevTest.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4a61f2c8d3095b40ab7e1c6d59f4e082 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Prefabs/WeaponPreset.prefab b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Prefabs/WeaponPreset.prefab index d18ce98f..1c790b97 100644 --- a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Prefabs/WeaponPreset.prefab +++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Prefabs/WeaponPreset.prefab @@ -58,5 +58,5 @@ MonoBehaviour: - rid: 2339642446330462276 type: {class: Shotgun, ns: Game.Gear, asm: Assembly-CSharp} data: + _damage: 37 _pellets: 8 - _spread: 12 diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/DocsMedia/Loadout.cs b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/DocsMedia/Loadout.cs deleted file mode 100644 index 4d8a4a95..00000000 --- a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/DocsMedia/Loadout.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using UnityEngine; -using System.Collections.Generic; -using Aspid.FastTools.Types; - -// Docs-media harness: mirrors the SerializeReference Selector example in -// Documentation/03-serialize-reference-selector.md — IWeapon implementations picked into -// [SerializeReference] fields. Lives in DevTests, but uses a neutral game-like -// namespace because the picker breadcrumbs are visible in the recorded media. - -// ReSharper disable once CheckNamespace -namespace Game.Gear -{ - public interface IWeapon - { - void Fire(); - } - - [Serializable] - public sealed class Pistol : IWeapon - { - [SerializeField] [Min(0)] private int _damage = 10; - - public void Fire() => Debug.Log($"Pistol: {_damage} dmg"); - } - - [Serializable] - public sealed class Shotgun : IWeapon - { - [SerializeField] [Min(1)] private int _pellets = 8; - [SerializeField] [Range(0f, 45f)] private float _spread = 12f; - - public void Fire() => Debug.Log($"Shotgun: {_pellets} pellets, {_spread}° spread"); - } - - [Serializable] - public sealed class PlasmaRifle : IWeapon - { - [SerializeField] [Min(0f)] private float _power = 40f; - [SerializeField] [Min(0f)] private float _range = 60f; - - public void Fire() => Debug.Log($"Plasma rifle: {_power} power, {_range} m"); - } - - public sealed class Loadout : MonoBehaviour - { - [TypeSelector] - [SerializeReference] private IWeapon _primary; - - [TypeSelector] - [SerializeReference] private List _sidearms; - } -} diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Editor.meta b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Editor.meta new file mode 100644 index 00000000..7712f435 --- /dev/null +++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 51ef23676223470e9da1f453d67ed7c3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Editor/SerializeReferencesIMGUITestEditor.cs b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Editor/SerializeReferencesIMGUITestEditor.cs new file mode 100644 index 00000000..67632024 --- /dev/null +++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Editor/SerializeReferencesIMGUITestEditor.cs @@ -0,0 +1,24 @@ +using UnityEditor; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.DevTests.SerializeReferences.Editors +{ + // Forces IMGUI rendering for the SerializeReferencesIMGUITest inspector. + // + // Unity picks IMGUI vs UIToolkit at the Editor level: when CreateInspectorGUI is NOT overridden but + // OnInspectorGUI is, the whole inspector — including every nested PropertyDrawer — falls back to + // IMGUI. That routes the managed-reference fields through SerializeReferenceIMGUIPropertyDrawer.OnGUI + // instead of CreatePropertyGUI. The list is drawn as a plain PropertyField on purpose: its + // picker-backed + button belongs to SerializeReferenceIMGUIList, which the sample's WeaponPresetEditor + // already covers. + [CustomEditor(typeof(SerializeReferencesIMGUITest))] + internal sealed class SerializeReferencesIMGUITestEditor : Editor + { + public override void OnInspectorGUI() + { + serializedObject.Update(); + DrawPropertiesExcluding(serializedObject, "m_Script"); + serializedObject.ApplyModifiedProperties(); + } + } +} diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Editor/SerializeReferencesIMGUITestEditor.cs.meta b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Editor/SerializeReferencesIMGUITestEditor.cs.meta new file mode 100644 index 00000000..d8ca000c --- /dev/null +++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Editor/SerializeReferencesIMGUITestEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3323470f9b104576ab7e707f26a65c5b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/DocsMedia.meta b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures.meta similarity index 100% rename from Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/DocsMedia.meta rename to Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures.meta diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/IWeapon.cs b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/IWeapon.cs new file mode 100644 index 00000000..a4481442 --- /dev/null +++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/IWeapon.cs @@ -0,0 +1,8 @@ +// ReSharper disable once CheckNamespace +namespace Game.Gear +{ + public interface IWeapon + { + void Fire(); + } +} diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/IWeapon.cs.meta b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/IWeapon.cs.meta new file mode 100644 index 00000000..f5664aef --- /dev/null +++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/IWeapon.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 123f8810b16d4b5bbbc9c82f45684d76 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/Loadout.cs b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/Loadout.cs new file mode 100644 index 00000000..345cec50 --- /dev/null +++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/Loadout.cs @@ -0,0 +1,21 @@ +using UnityEngine; +using System.Collections.Generic; +using Aspid.FastTools.Types; + +// Fixture host for the prefabs next door: WeaponPreset (two ordinary references), SharedWeaponPreset +// (both fields pointing at one instance) and BrokenWeaponPreset (a deliberately misspelled stored type). +// They are the manual counterpart to the Project References and Asset References windows, which need +// assets on disk rather than an inspector. The namespace stays neutral because picker breadcrumbs show it. + +// ReSharper disable once CheckNamespace +namespace Game.Gear +{ + public sealed class Loadout : MonoBehaviour + { + [TypeSelector] + [SerializeReference] private IWeapon _primary; + + [TypeSelector] + [SerializeReference] private List _sidearms; + } +} diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/DocsMedia/Loadout.cs.meta b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/Loadout.cs.meta similarity index 100% rename from Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/DocsMedia/Loadout.cs.meta rename to Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/Loadout.cs.meta diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/Pistol.cs b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/Pistol.cs new file mode 100644 index 00000000..bed651d0 --- /dev/null +++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/Pistol.cs @@ -0,0 +1,14 @@ +using System; +using UnityEngine; + +// ReSharper disable once CheckNamespace +namespace Game.Gear +{ + [Serializable] + public sealed class Pistol : IWeapon + { + [SerializeField] [Min(0)] private int _damage = 10; + + public void Fire() => Debug.Log($"Pistol: {_damage} dmg"); + } +} diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/Pistol.cs.meta b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/Pistol.cs.meta new file mode 100644 index 00000000..2c3c2ea8 --- /dev/null +++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/Pistol.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 55b54bd3678a4f278e7265cc4ad8b2b1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/PlasmaRifle.cs b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/PlasmaRifle.cs new file mode 100644 index 00000000..96c51969 --- /dev/null +++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/PlasmaRifle.cs @@ -0,0 +1,17 @@ +using System; +using UnityEngine; + +// ReSharper disable once CheckNamespace +namespace Game.Gear +{ + // A third implementation that shares no field with the other two: switching to it drops the + // carried-over data instead of preserving it. + [Serializable] + public sealed class PlasmaRifle : IWeapon + { + [SerializeField] [Min(0f)] private float _power = 40f; + [SerializeField] [Min(0f)] private float _range = 60f; + + public void Fire() => Debug.Log($"Plasma rifle: {_power} power, {_range} m"); + } +} diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/PlasmaRifle.cs.meta b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/PlasmaRifle.cs.meta new file mode 100644 index 00000000..d3cec0b2 --- /dev/null +++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/PlasmaRifle.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a1fb3a5fbb604def8b22aff9a4e6ae8a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/Shotgun.cs b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/Shotgun.cs new file mode 100644 index 00000000..a975fbd6 --- /dev/null +++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/Shotgun.cs @@ -0,0 +1,17 @@ +using System; +using UnityEngine; + +// ReSharper disable once CheckNamespace +namespace Game.Gear +{ + // _damage is declared with the same name and type as on Pistol so that switching between the two + // carries the value over, which is the behavior Documentation/03-serialize-reference-selector.md describes. + [Serializable] + public sealed class Shotgun : IWeapon + { + [SerializeField] [Min(0)] private int _damage = 20; + [SerializeField] [Min(1)] private int _pellets = 6; + + public void Fire() => Debug.Log($"Shotgun: {_damage} dmg, {_pellets} pellets"); + } +} diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/Shotgun.cs.meta b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/Shotgun.cs.meta new file mode 100644 index 00000000..cc325223 --- /dev/null +++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/Fixtures/Shotgun.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 251c80a8a071466cbc0eb51926043d9d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/RequiredViolationsDevTest.cs b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/RequiredViolationsDevTest.cs index 1d44dec9..9601601f 100644 --- a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/RequiredViolationsDevTest.cs +++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/RequiredViolationsDevTest.cs @@ -15,6 +15,10 @@ public sealed class RequiredDevTestPayload : IRequiredDevTestPayload // Dev-only fixture for manually verifying the Project References "Required violations" group and the Asset // References REQUIRED badge: both fields are left unset on Prefabs/RequiredViolationsDevTest.prefab on purpose. + // + // Assets/DevTests is an excluded folder in the project's SerializeReference settings, so the project-wide + // scan skips this prefab until that entry is removed. Asset References inspects one chosen asset and shows + // it either way. public sealed class RequiredViolationsDevTest : MonoBehaviour { [SerializeReference, TypeSelector(Required = true)] diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/SerializeReferencesIMGUITest.cs b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/SerializeReferencesIMGUITest.cs new file mode 100644 index 00000000..1b5e8a49 --- /dev/null +++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/SerializeReferencesIMGUITest.cs @@ -0,0 +1,41 @@ +using UnityEngine; +using Game.Gear; +using System.Collections.Generic; +using Aspid.FastTools.Types; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.DevTests.SerializeReferences +{ + // Dev-only harness for the notice states of the SerializeReference drawers — NOT part of the package + // or its samples, which cannot ship a deliberately broken asset. The SerializeReferences sample covers + // ordinary selection in both UI paths; what is left is what only a hand-authored asset can produce. + // + // The companion editor (Editor/SerializeReferencesIMGUITestEditor.cs) forces the whole inspector + // through IMGUI, so the notices come from SerializeReferenceIMGUIPropertyDrawer and InspectorNoticeGUI + // instead of the InspectorNotice element. + // + // To test: open Prefabs/SerializeReferencesDevTest.prefab — this component sits next to + // SerializeReferencesUIToolkitTest, which carries the same four states in the default inspector. + public sealed class SerializeReferencesIMGUITest : MonoBehaviour + { + // Left null on the prefab: the "required reference is not set" notice. + [TypeSelector(Required = true)] + [SerializeReference] private IWeapon _required; + + // Both fields hold one instance on the prefab: the "shared reference" notice and Make unique. + [TypeSelector] + [SerializeReference] private IWeapon _sharedLeft; + + [TypeSelector] + [SerializeReference] private IWeapon _sharedRight; + + // The prefab stores a class name no assembly resolves: the missing-type notice with Fix and + // Smart Fix, whose suggestion comes from the one-letter difference to Pistol. + [TypeSelector] + [SerializeReference] private IWeapon _missing; + + // The same break inside a list, where the element notice and the list guard apply instead. + [TypeSelector] + [SerializeReference] private List _missingInList; + } +} diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/SerializeReferencesIMGUITest.cs.meta b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/SerializeReferencesIMGUITest.cs.meta new file mode 100644 index 00000000..a947d52a --- /dev/null +++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/SerializeReferencesIMGUITest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b420f95844b84734b4a50fffdc0a01e3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/SerializeReferencesUIToolkitTest.cs b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/SerializeReferencesUIToolkitTest.cs new file mode 100644 index 00000000..df10228b --- /dev/null +++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/SerializeReferencesUIToolkitTest.cs @@ -0,0 +1,29 @@ +using UnityEngine; +using Game.Gear; +using System.Collections.Generic; +using Aspid.FastTools.Types; + +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.DevTests.SerializeReferences +{ + // Dev-only UIToolkit counterpart to SerializeReferencesIMGUITest: identical fields, default inspector — + // the reference rendering the forced-IMGUI component is compared against on + // Prefabs/SerializeReferencesDevTest.prefab. Keep the two field lists in step. + public sealed class SerializeReferencesUIToolkitTest : MonoBehaviour + { + [TypeSelector(Required = true)] + [SerializeReference] private IWeapon _required; + + [TypeSelector] + [SerializeReference] private IWeapon _sharedLeft; + + [TypeSelector] + [SerializeReference] private IWeapon _sharedRight; + + [TypeSelector] + [SerializeReference] private IWeapon _missing; + + [TypeSelector] + [SerializeReference] private List _missingInList; + } +} diff --git a/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/SerializeReferencesUIToolkitTest.cs.meta b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/SerializeReferencesUIToolkitTest.cs.meta new file mode 100644 index 00000000..6445fec6 --- /dev/null +++ b/Aspid.FastTools/Assets/DevTests/SerializeReferences/Scripts/SerializeReferencesUIToolkitTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 19ed43d7d91a47a7b0ed8f22852b914d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/CombatModifiers.cs b/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/CombatModifiers.cs index 85560a36..acb066ca 100644 --- a/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/CombatModifiers.cs +++ b/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/CombatModifiers.cs @@ -1,8 +1,9 @@ using Aspid.FastTools.Types; -// Docs-media harness: [TypeSelectorDisplay] demo types shown in the Types.md picker screenshot. -// DamageModifier mirrors the attribute example in the docs; KnockbackModifier stays undecorated -// (group only) to contrast a custom name/icon row with a default one. +// Docs-media harness for Images/aspid_fasttools_type_selector_display.png in +// Documentation/02-serializable-types.md. DamageModifier mirrors the attribute example in that +// page; KnockbackModifier stays undecorated (group only) to contrast a custom name/icon row with +// a default one. ModifierRack hosts the field the screenshot's picker is opened from. // ReSharper disable once CheckNamespace namespace Game.Combat diff --git a/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/GenericEffects.cs b/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/GenericEffects.cs index 1e99cb9e..7df2be5c 100644 --- a/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/GenericEffects.cs +++ b/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/GenericEffects.cs @@ -2,8 +2,9 @@ using UnityEngine; using Aspid.FastTools.Types; -// Docs-media harness: open-generic picking demo for the TypeSelectorWindow GIF in Types.md — -// picking Amplify walks through its argument page before returning the constructed type. +// Docs-media harness for Images/aspid_fasttools_type_selector_generic.gif in +// Documentation/02-serializable-types.md — picking Amplify walks through its argument page +// before returning the constructed type. // ReSharper disable once CheckNamespace namespace Game.Combat diff --git a/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/Loadout.cs b/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/Loadout.cs deleted file mode 100644 index 67d9b0f2..00000000 --- a/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/Loadout.cs +++ /dev/null @@ -1,19 +0,0 @@ -using UnityEngine; -using Aspid.FastTools.Types; - -// Docs-media harness: mirrors the member-reference example in Documentation/02-serializable-types.md — -// _category drives _weaponType's picker live in the Inspector. - -// ReSharper disable once CheckNamespace -namespace Game.Combat -{ - public sealed class Loadout : MonoBehaviour - { - // The category chosen here drives the picker of _weaponType below. - [SerializeField] private SerializableType _category; - - // Constrained live to whatever _category currently holds. - [TypeSelector(nameof(_category))] - [SerializeField] private string _weaponType; - } -} diff --git a/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/Loadout.cs.meta b/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/Loadout.cs.meta deleted file mode 100644 index 76ebd1fb..00000000 --- a/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/Loadout.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 00bf531e3d43e4d7f84ee0555274ceb7 \ No newline at end of file diff --git a/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/ModifierRack.cs b/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/ModifierRack.cs new file mode 100644 index 00000000..f4858c21 --- /dev/null +++ b/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/ModifierRack.cs @@ -0,0 +1,16 @@ +using UnityEngine; +using Aspid.FastTools.Types; + +// Docs-media harness: the component the picker in Images/aspid_fasttools_type_selector_display.png is +// opened from. Allow = TypeAllow.None keeps the abstract CombatModifier out, so the page shows exactly +// the three decorated candidates. + +// ReSharper disable once CheckNamespace +namespace Game.Combat +{ + public sealed class ModifierRack : MonoBehaviour + { + [TypeSelector(typeof(CombatModifier), Allow = TypeAllow.None)] + [SerializeField] private string _modifierType; + } +} diff --git a/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/ModifierRack.cs.meta b/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/ModifierRack.cs.meta new file mode 100644 index 00000000..cb08ae58 --- /dev/null +++ b/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/ModifierRack.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8c41e6b0d7a24f5cb2938af61d05e7c3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/WeaponMount.cs b/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/WeaponMount.cs index d3ecc66d..68b7d3b8 100644 --- a/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/WeaponMount.cs +++ b/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/WeaponMount.cs @@ -1,8 +1,9 @@ using UnityEngine; using Aspid.FastTools.Types; -// Docs-media harness: Required = true demo for the Types.md screenshot — _primaryWeapon is -// filled in the shot, _secondaryWeapon stays empty to show the inline "required" notice. +// Docs-media harness for Images/aspid_fasttools_type_selector_required.png in +// Documentation/02-serializable-types.md — _primaryWeapon is filled in the shot, _secondaryWeapon +// stays empty to show the inline "required" notice. // ReSharper disable once CheckNamespace namespace Game.Combat diff --git a/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/Weapons.cs b/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/Weapons.cs index 164a21e6..b3e124e3 100644 --- a/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/Weapons.cs +++ b/Aspid.FastTools/Assets/DevTests/Types/Scripts/DocsMedia/Weapons.cs @@ -1,4 +1,4 @@ -// Docs-media harness: user-facing weapon hierarchy shown in the Types.md pickers. +// Docs-media harness: the weapon hierarchy shown in the Documentation/02-serializable-types.md pickers. // Lives in DevTests, but uses a neutral game-like namespace because the picker // breadcrumbs (and thus the namespace) are visible in the recorded media. diff --git a/Aspid.FastTools/Assets/DevTests/Types/Scripts/EnemyBase.cs b/Aspid.FastTools/Assets/DevTests/Types/Scripts/EnemyBase.cs index ec183973..5d50d49f 100644 --- a/Aspid.FastTools/Assets/DevTests/Types/Scripts/EnemyBase.cs +++ b/Aspid.FastTools/Assets/DevTests/Types/Scripts/EnemyBase.cs @@ -8,12 +8,12 @@ namespace Aspid.FastTools.DevTests.Types // EnemyBaseEditor (editorForChildClasses: true) forces IMGUI for this class and every subtype, // so the subtype dropdown renders through ComponentTypeSelectorPropertyDrawer.OnGUI. Selecting a // subtype rewrites m_Script in place — fields with matching names (_health) persist across the swap. - public abstract class EnemyBase : MonoBehaviour + public abstract class EnemyBase : MonoBehaviour, IEnemy { [SerializeField] private ComponentTypeSelector _enemyType; [SerializeField] [Min(0)] private float _health = 100f; - protected float Health => _health; + public float Health => _health; } } diff --git a/Aspid.FastTools/Assets/DevTests/Types/Scripts/IEnemy.cs b/Aspid.FastTools/Assets/DevTests/Types/Scripts/IEnemy.cs new file mode 100644 index 00000000..cebd64c4 --- /dev/null +++ b/Aspid.FastTools/Assets/DevTests/Types/Scripts/IEnemy.cs @@ -0,0 +1,10 @@ +// ReSharper disable once CheckNamespace +namespace Aspid.FastTools.DevTests.Types +{ + // Gives the enemy hierarchy an interface, so one [TypeSelector] constraint can cover all three + // TypeAllow categories at once: two concrete classes, one abstract base, one interface. + public interface IEnemy + { + float Health { get; } + } +} diff --git a/Aspid.FastTools/Assets/DevTests/Types/Scripts/IEnemy.cs.meta b/Aspid.FastTools/Assets/DevTests/Types/Scripts/IEnemy.cs.meta new file mode 100644 index 00000000..da5849d9 --- /dev/null +++ b/Aspid.FastTools/Assets/DevTests/Types/Scripts/IEnemy.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3b7c1d9a45f24c8ea1d6f0b2c7e83415 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Assets/DevTests/Types/Scripts/TypesIMGUITest.cs b/Aspid.FastTools/Assets/DevTests/Types/Scripts/TypesIMGUITest.cs index 1ecf9a13..7e50c2d5 100644 --- a/Aspid.FastTools/Assets/DevTests/Types/Scripts/TypesIMGUITest.cs +++ b/Aspid.FastTools/Assets/DevTests/Types/Scripts/TypesIMGUITest.cs @@ -4,10 +4,13 @@ // ReSharper disable once CheckNamespace namespace Aspid.FastTools.DevTests.Types { - // Dev-only harness for the IMGUI rendering path of the Type drawers — NOT part of the - // package or its samples. The companion editor (Editor/TypesIMGUITestEditor.cs) forces the - // whole inspector through IMGUI, routing SerializableType and [TypeSelector] fields through - // TypeIMGUIPropertyDrawer.OnGUI instead of the UIToolkit CreatePropertyGUI path. + // Dev-only harness for the IMGUI rendering path of the Type drawers — NOT part of the package or + // its samples. The companion editor (Editor/TypesIMGUITestEditor.cs) forces the whole inspector + // through IMGUI, routing the fields below through TypeIMGUIPropertyDrawer.OnGUI and + // MonoScriptIMGUIPropertyDrawer.OnGUI instead of the UIToolkit CreatePropertyGUI path. + // + // The field list mirrors what the Types sample demonstrates in UI Toolkit, so anything the sample + // covers has an IMGUI counterpart here. // // To test: open Prefabs/TypesDevTest.prefab — this component sits next to TypesUIToolkitTest // (same fields, default UIToolkit inspector) for side-by-side comparison, and a FastEnemy child @@ -17,6 +20,17 @@ public sealed class TypesIMGUITest : MonoBehaviour // SerializableType: strongly typed wrapper — the generic argument constrains the picker. [SerializeField] private SerializableType _serializableType; + // Non-generic wrapper: the attribute carries the only constraint, and BaseType stays typeof(object). + [TypeSelector(typeof(Collider))] + [SerializeField] private SerializableType _untypedWrapper; + + // Wrappers inside a collection: each element gets its own picker row. + [SerializeField] private SerializableType[] _wrapperArray; + + // MonoScript-backed wrapper: only types a MonoScript resolves are offered, and a .cs file can + // be dragged onto the field from Project. + [SerializeField] private SerializableMonoScript _monoScript; + // [TypeSelector] on a raw string: the same picker window on an un-wrapped assembly-qualified name. [TypeSelector(typeof(Collider))] [SerializeField] private string _typeSelectorString; @@ -24,5 +38,29 @@ public sealed class TypesIMGUITest : MonoBehaviour // [TypeSelector] on a string[]: each element is its own picker constrained to the base type. [TypeSelector(typeof(ScriptableObject))] [SerializeField] private string[] _typeSelectorArray; + + // The three Allow fields share one constraint and differ only in what the picker adds to the + // concrete FastEnemy and TankEnemy: nothing, the abstract EnemyBase, or the IEnemy interface. + [TypeSelector(typeof(IEnemy), Allow = TypeAllow.None)] + [SerializeField] private string _concreteOnly; + + [TypeSelector(typeof(IEnemy), Allow = TypeAllow.Abstract)] + [SerializeField] private string _withAbstract; + + [TypeSelector(typeof(IEnemy), Allow = TypeAllow.Interface)] + [SerializeField] private string _withInterface; + + // Left empty on the prefab on purpose: the inline "required" notice is drawn by + // InspectorNoticeGUI here and by the InspectorNotice element in the UIToolkit twin. + [TypeSelector(typeof(Collider), Required = true)] + [SerializeField] private string _requiredType; + + // Member reference: _dependentType follows whatever _category currently holds, and falls back + // to a notice while the constraint cannot be resolved. + [TypeSelector(typeof(IEnemy))] + [SerializeField] private SerializableType _category; + + [TypeSelector(nameof(_category), Allow = TypeAllow.None)] + [SerializeField] private string _dependentType; } } diff --git a/Aspid.FastTools/Assets/DevTests/Types/Scripts/TypesUIToolkitTest.cs b/Aspid.FastTools/Assets/DevTests/Types/Scripts/TypesUIToolkitTest.cs index 1d0d0858..b2c815e8 100644 --- a/Aspid.FastTools/Assets/DevTests/Types/Scripts/TypesUIToolkitTest.cs +++ b/Aspid.FastTools/Assets/DevTests/Types/Scripts/TypesUIToolkitTest.cs @@ -6,14 +6,40 @@ namespace Aspid.FastTools.DevTests.Types { // Dev-only UIToolkit counterpart to TypesIMGUITest: identical fields, default inspector — the // reference rendering the forced-IMGUI component is compared against on Prefabs/TypesDevTest.prefab. + // Keep the two field lists in step; a field present in only one of them cannot be compared. public sealed class TypesUIToolkitTest : MonoBehaviour { [SerializeField] private SerializableType _serializableType; + [TypeSelector(typeof(Collider))] + [SerializeField] private SerializableType _untypedWrapper; + + [SerializeField] private SerializableType[] _wrapperArray; + + [SerializeField] private SerializableMonoScript _monoScript; + [TypeSelector(typeof(Collider))] [SerializeField] private string _typeSelectorString; [TypeSelector(typeof(ScriptableObject))] [SerializeField] private string[] _typeSelectorArray; + + [TypeSelector(typeof(IEnemy), Allow = TypeAllow.None)] + [SerializeField] private string _concreteOnly; + + [TypeSelector(typeof(IEnemy), Allow = TypeAllow.Abstract)] + [SerializeField] private string _withAbstract; + + [TypeSelector(typeof(IEnemy), Allow = TypeAllow.Interface)] + [SerializeField] private string _withInterface; + + [TypeSelector(typeof(Collider), Required = true)] + [SerializeField] private string _requiredType; + + [TypeSelector(typeof(IEnemy))] + [SerializeField] private SerializableType _category; + + [TypeSelector(nameof(_category), Allow = TypeAllow.None)] + [SerializeField] private string _dependentType; } } diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/CHANGELOG.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/CHANGELOG.md index 0be5c2e0..57566817 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/CHANGELOG.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/CHANGELOG.md @@ -7,6 +7,16 @@ All notable changes to **Aspid.FastTools** will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- Added `AndApplyWithoutUndo` counterparts for every `SerializedProperty` setter with immediate application, including `SetValue` overloads, references, enums, and array size helpers. + +### Changed +- Renamed `GetScriptName()` to `GetDisplayName()` and `GetScriptNameWithIndex()` to `GetDisplayNameWithIndex()`; update existing calls to the new names. Both methods now return `string.Empty` for null or destroyed objects. Component indexing uses a pooled list instead of temporary arrays and LINQ. + + ## [1.0.0-rc.8] — 2026-09-06 First release. Unity **6000.0**, assemblies `Aspid.FastTools` / `Aspid.FastTools.Editor`, prebuilt Roslyn DLLs `Aspid.FastTools.Generators` / `Aspid.FastTools.Analyzers`. Every inspector feature works in both IMGUI and UI Toolkit. diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/01-getting-started.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/01-getting-started.md deleted file mode 100644 index 0192601e..00000000 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/01-getting-started.md +++ /dev/null @@ -1,50 +0,0 @@ -# Getting Started - -## Installation - -Install Aspid.FastTools via UPM: in the Package Manager click **+ → Install package from git URL…** and paste one of the URLs below. - -> [!NOTE] -> **Migrating from `com.aspid.fasttools`:** the package was renamed to `tech.aspid.fasttools` in May 2026. Unity treats it as a different package, so installs of the old id never receive updates — remove the `com.aspid.fasttools` entry from `Packages/manifest.json` and install `tech.aspid.fasttools` via one of the URLs below. - -### Stable - -The `upm` branch always points to the latest **stable** release: - -``` -https://github.com/VPDPersonal/Aspid.FastTools.git#upm -``` - -To install a specific version, target the immutable per-release tag `upm/` — e.g. `upm/1.0.0` once the 1.0.0 release is out (see [Releases](https://github.com/VPDPersonal/Aspid.FastTools/releases) for the list of available versions): - -``` -https://github.com/VPDPersonal/Aspid.FastTools.git#upm/ -``` - -Prefer a manual install? Download the `.unitypackage` from the [Releases](https://github.com/VPDPersonal/Aspid.FastTools/releases) page, or get the package from the [Unity Asset Store](https://assetstore.unity.com/packages/slug/365584). - -### Preview - -The `upm-preview` branch always points to the latest **preview** release (rc, beta, alpha, …): - -``` -https://github.com/VPDPersonal/Aspid.FastTools.git#upm-preview -``` - -Specific preview versions use the same per-release tag scheme: - -``` -https://github.com/VPDPersonal/Aspid.FastTools.git#upm-preview/1.0.0-rc.8 -``` - -## Samples - -Each feature ships with a sample: a small scene or editor tool that does something visible with the feature, plus a `README.md` that walks through what to try and where to look in the code. Import them from the Package Manager (**Aspid.FastTools → Samples**) or open the **Welcome** tab (`Tools → Aspid 🐍 → FastTools → Welcome`). - -| Sample | What it shows | -|---|---| -| [Types](../Samples~/Types/README.md) | An enemy spawner: `SerializableMonoScript`, `SerializableType`, `[TypeSelectorDisplay]`, a member-referenced `[TypeSelector]`, `ComponentTypeSelector` | -| [SerializeReferences](../Samples~/SerializeReferences/README.md) | A turret with polymorphic weapons: the `[SerializeReference]` picker in every field shape, broken assets for the repair tools, an IMGUI inspector | -| [EnumValues](../Samples~/EnumValues/README.md) | A walker over surface tiles: both `EnumValues` variants, default values, `[Flags]` lookup rules | -| [ProfilerMarkers](../Samples~/ProfilerMarkers/README.md) | A flock simulation: the generated marker tree in the Profiler | -| [EditorTools](../Samples~/EditorTools/README.md) | An editor window and inspector: fluent `VisualElement` extensions, `SerializedProperty` setters, editor helpers, `TypeSelectorWindow` | diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/02-serializable-types.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/02-serializable-types.md index 61716247..7b02f422 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/02-serializable-types.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/02-serializable-types.md @@ -1,203 +1,197 @@ # Serializable Type System -Unity cannot serialize `System.Type` out of the box — the Serializable Type System closes that gap: the type is picked in the Inspector through a hierarchical, searchable window, stored as an assembly-qualified name, and lazily resolved to a `System.Type` on first access. - -**Reference sections:** - -* [`SerializableType`](#serializabletype) — a serializable field wrapper over `System.Type`; -* [`SerializableMonoScript`](#serializablemonoscript) — the same wrapper referenced through the - script asset, so a class rename does not break the field; -* [`TypeSelectorAttribute`](#typeselectorattribute) — a type-picker button on `string`, - `SerializableType` and `[SerializeReference]` fields, including - [dynamic base types via member references](#dynamic-base-types-via-member-references); -* [`TypeSelectorDisplay`](#typeselectordisplay) — a candidate type's name, group, tooltip and icon - in the picker; -* [`TypeSelectorWindow`](#typeselectorwindow) — the same picker window as a public API for - your own editor code; -* [`ComponentTypeSelector`](#componenttypeselector) — an Inspector dropdown that switches - a component or ScriptableObject to a subtype. +Type selection in the Inspector. The type persists with a component or asset and is read in code as `System.Type`. The wrappers store the type only — your code creates the instance. For an instance with editable data, use [SerializeReference Selector](03-serialize-reference-selector.md). + +## Quick start + +Unity does not serialize a `System.Type` field directly. Instead of manually filling and resolving a string, declare `SerializableType`. The argument `T` constrains selection to compatible types. This example uses `Collider`: + +| Before — a type-name string | After — SerializableType | +|---|---| +|
[SerializeField]
private string _colliderTypeName;

public System.Type ColliderType =>
    string.IsNullOrEmpty(_colliderTypeName)
        ? null
        : System.Type.GetType(
            _colliderTypeName, false);
|
[TypeSelector(Allow = TypeAllow.None)]
[SerializeField]
private SerializableType<Collider>
    _colliderType;

public System.Type ColliderType =>
    _colliderType?.Type;
| + +The wrapper already has a picker; the attribute here excludes abstract classes and interfaces. + +![Selecting a serializable type in the Inspector](Images/serializable-type-quick-start.gif) + +Selecting a serializable type in the Inspector + +## Choosing a tool + +| Task | Tool | +|---|---| +| Store a type, including generic types and types declared inside another class | [`SerializableType`](#serializabletype) | +| Retain selection when renaming a class and its file | [`SerializableMonoScript`](#serializablemonoscript) | +| Add a type picker to a string or constrain a field | [`TypeSelector`](#typeselectorattribute) | +| Store an instance in `[SerializeReference]` | [SerializeReference Selector](03-serialize-reference-selector.md) | +| Customize a candidate's name, group, icon, or visibility | [`TypeSelectorDisplay`](#typeselectordisplay) | +| Open the window from editor code | [`TypeSelectorWindow`](#typeselectorwindow) | ## SerializableType -A serializable wrapper over `System.Type`: it stores the selected type as an assembly-qualified name and lazily resolves it to a `System.Type` on first access. Two variants are available: +`SerializableType` stores an assembly-qualified name: the type name together with its assembly. -- **`SerializableType`** — stores any type; -- **`SerializableType`** — stores a type constrained to `T` or its subclasses. +| Variant | Selection constraint | +|---|---| +| `SerializableType` | No base-type constraint | +| `SerializableType` | Types assignable to `T`, including interface implementations | -Both support implicit conversion to `System.Type`, can be created from code with a `Type`-taking constructor -(`new SerializableType(typeof(Dash))` — the constrained one throws for a type not assignable to `T`), -and expose the stored name through `AssemblyQualifiedName`. `SerializableType` derives from `SerializableType`; -both build on the abstract `SerializableTypeBase`. Keep in mind that Unity serializes a field by its declared type: -a `SerializableType` assigned from code to a plain `SerializableType` field reloads as the unconstrained wrapper. +Both variants convert implicitly to `System.Type` and have a public constructor taking a `Type`: ```csharp -using UnityEngine; -using Aspid.FastTools.Types; +var selected = new SerializableType(typeof(BoxCollider)); +System.Type type = selected; + +var empty = new SerializableType(null); +``` -public abstract class Ability : MonoBehaviour -{ - public abstract void Activate(); -} +The type must be compatible with `T`, otherwise the constructor throws `ArgumentException`. Pass `null` for an empty wrapper; there is no public parameterless constructor. -public sealed class AbilitySelector : MonoBehaviour -{ - [SerializeField] private SerializableType _abilityType; +| Property or call | Result | +|---|---| +| `Type` | The resolved `System.Type`; `null` for an empty selection or unresolved name | +| `AssemblyQualifiedName` | The stored name, even if the type is missing; an empty string for no selection | +| `BaseType` | `typeof(object)`, or `typeof(T)` for the generic variant | +| `ToString()` | The resolved type's short name; otherwise the stored name | - private void Start() - { - var ability = (Ability)gameObject.AddComponent(_abilityType.Type); - ability.Activate(); - } -} -``` +### Empty values and renames -![SerializableType field with type selection in the Inspector](Images/aspid_fasttools_serializable_type.gif) +Renaming a class, namespace, or assembly can break the stored name. The Inspector then shows ``; check `.Type` for `null` before using it. + +> [!NOTE] +> Unity serializes a wrapper by the field's declared type. Assigning `SerializableType` to a `SerializableType` field preserves the selected type after loading, but loses the `T` constraint. Declare the generic variant on the field itself. The same rule applies to `SerializableMonoScript`. ## SerializableMonoScript -The same field, referenced through the script asset instead of the type name. `SerializableMonoScript` and -`SerializableMonoScript` keep an editor-only `MonoScript` reference next to the assembly-qualified name; in the -editor the script is the source of truth, so **renaming or moving the class keeps the field intact** — the stored name -is re-read from the script whenever the object is serialized. The reference exists only under `UNITY_EDITOR`: a player -build serializes just the name, and at runtime the wrapper resolves from it exactly like `SerializableType`. +`SerializableMonoScript` links the selected type to a script asset, retaining selection when the class and file are renamed or moved together. Choose the type in the Inspector or drag its `.cs` file from **Project**. -The trade-off is Unity's own: only a type that maps to a script asset qualifies — a top-level, non-generic class -declared in a file of the same name (what `MonoScript.GetClass()` reports). The picker lists only such types, and a -`MonoScript` can be dragged from the Project window onto the field. Nested and generic types need `SerializableType`. +| Stored name | Script-asset reference | +|---|---| +|
[TypeSelector(Allow = TypeAllow.None)]
[SerializeField]
private SerializableType<MonoBehaviour>
    _componentType;
|
[TypeSelector(Allow = TypeAllow.None)]
[SerializeField]
private SerializableMonoScript<MonoBehaviour>
    _componentType;
| -```csharp -public sealed class EnemySpawner : MonoBehaviour -{ - // Survives renaming Grunt to Soldier; the picker offers Enemy subtypes backed by a script file. - [SerializeField] private SerializableMonoScript _enemyType; - - private void Spawn() => - gameObject.AddComponent(_enemyType.Type); -} -``` +| Capability | SerializableType | SerializableMonoScript | +|---|---|---| +| Searchable picker | Yes | Yes, only types backed by a suitable MonoScript | +| Generic types and types declared inside another class | Yes | No | +| Built-in Unity types without a `MonoScript` asset, such as `BoxCollider` | Yes | No | +| Name update after a script rename | Manual | From the stored MonoScript during serialization | +| Construction from a `Type` in code | Public constructor | No public constructor | +| In a player | Type name | Type name; the MonoScript reference is editor-only | + +`BoxCollider` ships in the `UnityEngine.PhysicsModule` assembly, so there is no corresponding script asset in the project. -A wrapper constructed from code (`new SerializableMonoScript(typeof(Dash))`) carries the type name only and becomes -rename-safe once a type is picked in the Inspector. +The script must declare a top-level, non-generic class in a matching file, and `MonoScript.GetClass()` must return that class. Keep the asset and its `.meta` when renaming. If Unity can no longer resolve the class, the wrapper retains the last known name. -`[TypeSelector]` (including `Required = true`) applies to these fields the same way it does to `SerializableType`. -`SerializableMonoScript` derives from `SerializableMonoScript`, which shares the abstract `SerializableTypeBase` with -`SerializableType` but is not one (its serialized layout differs). The referenced asset is exposed through the -editor-only `Script` property. +Read the selected type through `.Type` or implicit conversion to `System.Type`, as with `SerializableType`. ## TypeSelectorAttribute -Adds a type-picker button to a field in the Inspector: it opens a hierarchical, searchable window listing only the types assignable to the given base types (when several are given, to all of them at once; with no arguments, any type qualifies). What happens on selection depends on the field's shape: +The attribute configures field selection. Wrappers already have a picker without the attribute; on a plain string, it adds one. -- `string` — the assembly-qualified name of the selected type is written into the field; -- `SerializableType` / `SerializableType` and `SerializableMonoScript` / `SerializableMonoScript` — narrows the built-in selector; the attribute's base types intersect with the generic argument `T`; -- `[SerializeReference]` managed reference — the selected type is instantiated into the field immediately (see [SerializeReference Selector](03-serialize-reference-selector.md)). +| Field | Selection result | +|---|---| +| `string` | Stores the assembly-qualified name | +| `SerializableType` / `SerializableMonoScript` | Configures the wrapper's selection | +| `[SerializeReference]` | Creates an instance of the selected implementation | -The attribute is editor-only (`[Conditional("UNITY_EDITOR")]`) and carries no runtime cost. +### Constraints and collections ```csharp -using UnityEngine; -using Aspid.FastTools.Types; +[TypeSelector(typeof(MonoBehaviour), Allow = TypeAllow.None)] +[SerializeField] private string _componentTypeName; + +[TypeSelector(typeof(IDamageable), Allow = TypeAllow.None)] +[SerializeField] private SerializableType _damageableType; -public interface IStackable { } - -public abstract class AbilityModifier -{ - public abstract void Apply(); -} - -public sealed class AbilitySelector : MonoBehaviour -{ - // string — the assembly-qualified name of the selected type is stored. - // Each array element is its own picker, constrained to AbilityModifier. - [TypeSelector(typeof(AbilityModifier))] - [SerializeField] private string[] _modifierTypes; - - // SerializableType — narrows the picker the field already has. - [TypeSelector(typeof(AbilityModifier))] - [SerializeField] private SerializableType _modifierType; - - // SerializableType — T already narrows the picker on its own; the base - // types of the attribute intersect with it: only AbilityModifier - // implementations that are also IStackable qualify. - [TypeSelector(typeof(IStackable))] - [SerializeField] private SerializableType _stackableModifierType; - - // For a [SerializeReference] field picking a type immediately creates - // an instance and assigns it to the field. With no arguments the attribute - // offers subtypes of the field's own type (here — AbilityModifier). - // Required = true flags an unset field: an inspector warning - // plus a violation for the build/CI gate. - [TypeSelector(Required = true)] - [SerializeReference] private AbilityModifier _modifier; -} +[TypeSelector(Allow = TypeAllow.None)] +[SerializeField] private SerializableType[] _colliderTypes; ``` +`IDamageable` is your interface. `_damageableType` offers components that both inherit `MonoBehaviour` and implement `IDamageable`. All constraints on a string or wrapper apply together (**AND**). Arrays and lists get a picker for each entry. + +For `[SerializeReference]`, the types in the attribute are **alternatives**. Suppose `Pistol` and `Rifle` are serializable classes implementing `IWeapon`: + +```csharp +[TypeSelector(typeof(Pistol), typeof(Rifle))] +[SerializeReference] private IWeapon _weapon; +``` + +The field offers `Pistol` **or** `Rifle`; a candidate does not have to match both types. Another class, `Sword : IWeapon`, is excluded: matching the field type alone is not enough. See [instance selector configuration](03-serialize-reference-selector.md#configuring-selection). + ### Constructors and properties +| Property | Default | Behaviour | +|---|---|---| +| `Allow` | `TypeAllow.All` | `Abstract` adds abstract classes; `Interface` adds interfaces. `All` enables both categories; `None` excludes them. Ignored on `[SerializeReference]` | +| `Required` | `false` | Warns about an empty type name or a `null` managed reference | + +Static classes are excluded. On a string or wrapper, `Allow` filters type categories without checking for a parameterless constructor. + +
+TypeSelector argument forms + ```csharp -[Conditional("UNITY_EDITOR")] -public sealed class TypeSelectorAttribute : PropertyAttribute -{ - public TypeSelectorAttribute() // base type: object - public TypeSelectorAttribute(Type type) - public TypeSelectorAttribute(params Type[] types) - public TypeSelectorAttribute(string assemblyQualifiedName) - public TypeSelectorAttribute(params string[] assemblyQualifiedNames) - - public TypeAllow Allow { get; set; } // default: TypeAllow.All - public bool Required { get; set; } // default: false -} - -[Flags] -public enum TypeAllow -{ - None = 0, - Abstract = 1, - Interface = 2, - All = Abstract | Interface -} +[TypeSelector] +[TypeSelector(typeof(MonoBehaviour))] +[TypeSelector(typeof(MonoBehaviour), typeof(IDamageable))] +[TypeSelector("Namespace.TypeName, AssemblyName")] +[TypeSelector(nameof(_category))] ``` -| Property | Description | -|----------|-------------| -| `Allow` | Which special type categories (abstract classes, interfaces) the picker includes in addition to plain concrete classes. Default: `TypeAllow.All` (a type-name field lists abstract classes and interfaces too; set `TypeAllow.None` to restrict it to concrete types). Ignored on a `[SerializeReference]` managed reference | -| `Required` | Flags an unset field: a `[SerializeReference]` managed reference left `null`, or a `string` field left empty, shows an inline "required" warning in the Inspector and counts as a violation for the build/CI gate. Also covers a `SerializableType` / `SerializableMonoScript` field (its stored type name left empty). Default: `false` | +Apply one `[TypeSelector]` per field. It accepts `Type` or `string` arguments: one value, multiple comma-separated values (`params`), or an array. Without arguments, it adds no constraints. A string is first resolved as a field or property name, then as a type name if no such member exists. + +
+ +### The Required notice + +```csharp +[TypeSelector(Required = true, Allow = TypeAllow.None)] +[SerializeField] private SerializableType _requiredType; +``` -#### The Required notice +![An empty required field shows a notice beside the picker](Images/type-selector-required.png) -An empty field with `Required = true` looks like this in the Inspector: +An empty required field shows a notice beside the picker -![A filled picker field next to an empty Required field showing the inline notice](Images/aspid_fasttools_type_selector_required.png) +With `Required = true`, `` remains selectable: clearing the field shows a warning beside it. For strings and wrappers, the check tests for an empty stored name; a missing type with a nonempty name passes this check. -To find and fix such violations project-wide from the FastTools window instead of chasing them -one Inspector at a time, see [Bulk repair tabs](04-serialize-reference-tooling.md#bulk-repair-tabs). +For project-wide and CI validation, see [required-field checks](04-serialize-reference-tooling.md#where-required-fields-are-checked). ## Dynamic base types via member references -The string constructors resolve **member-first**: when the string is a valid C# identifier that matches an instance field or property on the same object, that member's *current value* supplies the base type(s) — so one field can constrain another's picker, live in the Inspector. Any other string is treated as an assembly-qualified type name (`Type.GetType`), which is what you need for a type the call site cannot reference with `typeof` (across an editor or asmdef boundary). +Pass `nameof(...)` to let a field or property's current value control the candidate list. For example, a base category and a dependent selection: ```csharp -public sealed class Loadout : MonoBehaviour -{ - // The category chosen here drives the picker of _weaponType below. - [SerializeField] private SerializableType _category; - - // Constrained live to whatever _category currently holds. - [TypeSelector(nameof(_category))] - [SerializeField] private string _weaponType; -} +[SerializeField] private SerializableType _category; + +[TypeSelector(nameof(_category), Allow = TypeAllow.None)] +[SerializeField] private string _componentTypeName; ``` -The referenced member must be an instance field or property of type `Type`, `string`, or `SerializableType` / `SerializableType` — or an array of any of these. Prefer `nameof(...)` so a rename keeps the link. An unknown member name, or a member of an unsuitable shape, is a **compile error** (analyzer rules `AFT0006`–`AFT0008`); for cases the analyzer cannot see (precompiled assemblies, a rename without recompilation) the drawer shows an inline warning below the field instead. +Change **Category**, then open **Component Type Name**: the list is constrained to the selected type and its subclasses. Changing a constraint does not clear an earlier selection by itself; review the dependent field and select a new type if needed. + +| Constraint source | Support | +|---|---| +| `System.Type` | One type | +| `string` | A type name resolved through `Type.GetType` | +| `SerializableType`, `SerializableMonoScript`, and their generic variants | The resolved `.Type` value | +| An array of these values | Multiple simultaneous constraints | + +The source must be an instance field or readable property on the object being edited. Inherited members work; indexers do not. An empty source contributes no constraint. A generic wrapper's own `T` continues to constrain selection. + +Use `typeof` for a type and `nameof` for a field or property. If a string names neither an object member nor an available type, the Inspector shows a warning. + +![The typo _categroy instead of _category triggers a warning. Use nameof(_category) to avoid this mistake.](Images/type-selector-constraint-warning.png) + +The typo _categroy instead of _category triggers a warning. Use nameof(_category) to avoid this mistake. ## TypeSelectorDisplay -Decorate a candidate type with `[TypeSelectorDisplay]` to tune how it appears in the picker — an editor-only attribute (`[Conditional("UNITY_EDITOR")]`) in `Aspid.FastTools.Types` that carries no runtime cost. The compiler evaluates that condition where the attribute is *written*, so declare it from inside the Unity project: a type compiled outside Unity — a plugin `.dll` built by `dotnet build` — carries none of these settings, `Hidden` included. +`TypeSelectorDisplay` customizes a type's label, group, icon, and tooltip in the picker: ```csharp using Aspid.FastTools.Types; -// Rename the type in the picker, place it under an explicit group, give it a tooltip and an icon: [TypeSelectorDisplay( Name = "Damage ×", Group = "Combat/Modifiers", @@ -206,102 +200,107 @@ using Aspid.FastTools.Types; public sealed class DamageModifier { } ``` -| Member | Description | -|--------|-------------| -| `Name` | Display name shown instead of the type's short name — in the picker rows and in the closed dropdown's caption. Search still matches the real type name too, and the hover tooltip keeps revealing the full `Namespace.Class, Assembly` identity. `null` or whitespace means no override. | -| `Group` | Explicit picker path with `/` separating levels (e.g. `"Combat/Melee"`). **Replaces** the type's namespace placement — the type appears only under this path, and path segments are shared between types. `null` or whitespace keeps the namespace placement. | -| `Tooltip` | Tooltip shown when hovering the type's row. `null` means no tooltip override. | -| `Icon` | Editor icon shown left of the label — an `EditorGUIUtility.IconContent` name, a project-relative asset path with extension (loaded via `AssetDatabase`), or a `Resources` texture path without extension. `null` means no icon. | -| `Hidden` | When `true`, the picker never offers the type. Not inherited, so hiding a base type leaves the subclasses meant to replace it offered as usual. Assigning the type from code is unaffected, and a value already stored in a field keeps rendering. | +![The Damage × name, icon, and Combat/Modifiers group in the picker](Images/type-selector-display.png) -Use `Hidden` for a type that is assignable but not meant to be authored in the Inspector — a delegate-backed adapter, a test double, a base implementation kept only for code: +The Damage × name, icon, and Combat/Modifiers group in the picker -```csharp -[TypeSelectorDisplay(Hidden = true)] -public sealed class DelegateModifier : IModifier { } -``` +| Property | Result | +|---|---| +| `Name` | Caption in the list and closed field. Search still matches the real type name | +| `Group` | Grouping instead of the namespace; `/` separates levels, such as `Combat/Melee` | +| `Tooltip` | Text shown on hover | +| `Icon` | An `EditorGUIUtility.IconContent` name, an asset path with extension, or a `Resources` path without extension | +| `Hidden` | When `true`, hides the type from normal selection. Not inherited; code assignment and display of stored values still work | -`Hidden` governs authoring, not recovery. A **repair** picker — the missing-reference **Fix** and the References window's bulk fix — still offers hidden types, because a reference already stored as one has to stay re-pointable. **Smart Fix**, which proposes a type rather than letting you choose, never suggests one. +> [!NOTE] +> `TypeSelectorDisplay` depends on `UNITY_EDITOR` in the assembly where the attribute is applied. A class compiled into an external DLL without that symbol carries none of these settings, including `Hidden`. -In the picker, the `DamageModifier` from the example above appears under `Combat/Modifiers` as "Damage ×" with its icon — next to siblings that keep their default look: +## TypeSelectorWindow -![Custom name, icon and group in the picker via TypeSelectorDisplay](Images/aspid_fasttools_type_selector_display.png) +The picker groups types by namespace or `Group` and distinguishes identical names by assembly. Use `TypeSelectorWindow` to open it from a custom inspector or editor window. -## TypeSelectorWindow +![Favorites and Recent on the picker root page](Images/type-selector-window.png) + +Favorites and Recent on the picker root page + +| Action | Control | +|---|---| +| Move / select / close | Arrow keys / Enter / Escape | +| Return to the parent group | Left arrow or breadcrumbs | +| Toggle a favourite | Space or the star on hover | +| Clear the value | `` | + +Configure **Favorites**, **Recent**, and history capacity in the FastTools window's **Settings** tab. -A searchable, namespace-hierarchical type-picker popup — the same picker opened by `[TypeSelector]` and `SerializableType`, also available as a public API. The window offers: +### Generic types -- Hierarchical namespace organization -- Text search with filtering -- Keyboard navigation (Arrow keys, Enter, Escape; Space toggles a favorite) -- Breadcrumb trail with back navigation (Left arrow or a click on a crumb) -- Assembly disambiguation for types with identical names -- **Favorites** (★ on hover) and **Recent** (last picks) sections on the root page — stored locally per project (`EditorPrefs`, never committed), hidden while searching -- A `` option pinned at the top and a ✓ mark on the current value — its row is pre-selected on open -- Type counters on namespace/group rows and section headers -- Generic type support — picking an open generic walks through its type parameters and emits the constructed type -- Favorites/Recent tuning (on/off, Recent capacity) in the Settings tab of the SerializeReference window +Picking an open generic type opens its argument pages and returns a constructed closed type. For example, choosing `int` for `Container` produces `Container`. A generic argument can itself be generic; the window resolves its parameters first. -![Root page of the picker with Favorites, Recent and namespace counters](Images/aspid_fasttools_type_selector_window.png) +![Choosing a generic type argument in the picker](Images/type-selector-generic.gif) -Picking an open generic walks through its argument page and returns the constructed type: +Choosing a generic type argument in the picker -![Picking an open generic via its argument page](Images/aspid_fasttools_type_selector_generic.gif) +Arguments must satisfy the generic parameter's constraints; `[Serializable]` is not required. For `[SerializeReference]`, see the [serialization and inference rules](03-serialize-reference-selector.md#generic-types). -> The argument page only lists types Unity can serialize as a field value: primitives, `enum`, `string`, `UnityEngine.Object`-derived references, and `[Serializable]` classes/structs. Abstract types, interfaces, open generics, and delegates never appear as candidates. Give a candidate type the `[Serializable]` attribute to make it selectable. +### Opening from code -The window is available as a public API — open it from any editor code (custom inspectors, `EditorWindow`, menu items) when you need a type picker outside the standard `SerializableType` / `[TypeSelector]` flow. +In an editor script, import `Aspid.FastTools.Types.Editors`. `screenRect` is the button rectangle in **screen coordinates**, and `selectedTypeName` is the current type-name string: ```csharp -namespace Aspid.FastTools.Types.Editors -{ - public sealed class TypeSelectorWindow : EditorWindow +TypeSelectorWindow.Show( + screenRect, + new TypeSelectorFilter { - public static void Show( - Rect screenRect, - TypeSelectorFilter filter = default, - string currentAqn = "", - Action onSelected = null); - } -} + Types = new[] { typeof(MonoBehaviour) }, + Allow = TypeAllow.None + }, + currentAqn: selectedTypeName, + onSelected: aqn => selectedTypeName = aqn); ``` -| Parameter | Description | -|-----------|-------------| -| `screenRect` | Screen-space rectangle the dropdown is anchored to. | -| `filter` | Bundles which types the selector offers: base types (`Types`, only types assignable to **all** entries are listed; defaults to `typeof(object)`), the included kinds (`Allow`), an optional per-type `Predicate`, verbatim `AdditionalTypes`, the open-generic `ArgumentFilter` (which types an argument page offers) and `InferredArgumentFilter` (whether an argument the field itself determines is admissible for that particular parameter), and `HideNoneOption` (leave the `` row out when the target must always hold a type). | -| `currentAqn` | Assembly-qualified name of the currently selected type, used to pre-navigate to its location. Pass `null` or empty to start at the root. | -| `onSelected` | Callback invoked with the assembly-qualified name of the selected type, or `null` if the user chose ``. | +The callback receives an assembly-qualified name, or `null` for ``. Dismissing the window without a choice does not assign a value. If the result belongs to an asset, write it through `SerializedProperty` and apply the changes. -## ComponentTypeSelector +`currentAqn` controls the current mark: an empty string marks ``, while `null` leaves selection unmarked. -A serializable struct that adds a type-switch dropdown to the Inspector. Add it as a field on a base class — picking a subtype rewrites `m_Script` on the `SerializedObject`, effectively turning the component or ScriptableObject into the selected subtype. +### Window filters -The list is automatically restricted to subtypes of the class declaring the field. No extra configuration is required. +`TypeSelectorFilter` is a struct. Its `default` has `Allow = None`, unlike the `[TypeSelector]` attribute, which defaults to `All`. Set the mode explicitly when you need abstract classes or interfaces. -```csharp -using UnityEngine; -using Aspid.FastTools.Types; +
+Window filter properties -public abstract class EnemyBase : MonoBehaviour -{ - [SerializeField] private ComponentTypeSelector _enemyType; - [SerializeField] [Min(0)] private float _health = 100f; +| Property | Purpose | +|---|---| +| `Types` | Base types every candidate must satisfy | +| `Allow` | Allowed categories: abstract classes and interfaces | +| `Predicate` | An additional condition after type and category checks | +| `AdditionalTypes` | Candidates that bypass `Types`, `Allow`, and `Predicate`; `Hidden` filtering remains | +| `ArgumentFilter` | An additional filter for manually selected arguments | +| `InferredArgumentFilter` | A filter for arguments inferred from the field type | +| `IncludeHidden` | Offer types marked `Hidden = true` | +| `HideNoneOption` | Hide `` on the root page | - public abstract void Attack(); -} +Use `Predicate` to narrow the list; `AdditionalTypes` adds candidates that bypass constraints. -public sealed class FastEnemy : EnemyBase -{ - [SerializeField] [Min(0)] private float _speed = 25f; +
- public override void Attack() => - Debug.Log($"Fast enemy strikes! (speed: {_speed})"); -} -``` +For a window that edits assets, see [EditorTools](../Samples~/EditorTools/Documentation/README.md). + +## Troubleshooting selection + +| Symptom | What to check | +|---|---| +| A class is missing | Compatibility with the base and every constraint, `Allow`, `Hidden`, and compilation errors | +| A type appears in SerializableType but not SerializableMonoScript | Whether it has a separate script file and `MonoScript.GetClass()` returns the intended class | +| Changing Category leaves the old value | Constraints change the candidate list, not the dependent field's stored value | +| `` with a nonempty name | Whether the class, namespace, or assembly changed; select an existing type again | +| Required does not warn about a missing type | For strings and wrappers, it checks an empty name rather than successful resolution | +| A type is selected but no object appears | Storing a type does not instantiate it; use your creation code or [SerializeReference Selector](03-serialize-reference-selector.md) | + +## Package sample -![ComponentTypeSelector switches the component type in the Inspector](Images/aspid_fasttools_component_type_selector.gif) +For Inspector selection of enemy types and spawn patterns, see [Types](../Samples~/Types/Documentation/README.md). -Notes on the type-switching dropdown's behavior: +![A wave of regular and elite enemies moves toward the center.](../Samples~/Types/Documentation/Images/demo.gif) -- Because the dropdown owns type-switching, the Inspector's built-in **Script** row is hidden while the selector is present — you change the type only through the dropdown (UIToolkit inspectors only; the legacy IMGUI inspector draws that row itself). +A wave of regular and elite enemies moves toward the center. diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/03-serialize-reference-selector.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/03-serialize-reference-selector.md index 9b5c67bc..96551e78 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/03-serialize-reference-selector.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/03-serialize-reference-selector.md @@ -1,94 +1,266 @@ # SerializeReference Selector -The stock Inspector cannot populate `[SerializeReference]` fields: a managed reference -cannot be created from the UI, and when a type is renamed or deleted Unity silently clears -the data. SerializeReference Selector closes both gaps: a dropdown implementation picker -right in the Inspector, plus per-field repair actions for broken references. Project-wide -auditing, mass repair and the build/CI gate live in -[SerializeReference Tooling](04-serialize-reference-tooling.md). +Choose an interface or base-class implementation directly in a `[SerializeReference]` field. The selector creates an instance, expands its fields, and carries compatible data over when switching types. This page covers individual Inspector fields; project audits, bulk repair, and CI are covered in [SerializeReference Tooling](04-serialize-reference-tooling.md). -**Reference sections:** + -* [`Inspector type dropdown`](#inspector-type-dropdown) — the `[TypeSelector]` dropdown - on `[SerializeReference]` fields: implementation picking, nested inspector, generics, - copy/paste; -* [`Repairing broken references`](#repairing-broken-references) — a yellow notice instead - of a silent clear, **Fix** / **Smart Fix** / **Make unique**. +## Quick start -## Inspector type dropdown +Add `[TypeSelector]` next to `[SerializeReference]` to choose implementations in a searchable window without writing a custom editor. -Add `[TypeSelector]` next to `[SerializeReference]` — the Inspector replaces the stock -managed-reference UI with the hierarchical [type-selection window](02-serializable-types.md#typeselectorwindow) -with search. You pick which concrete implementation of the field's type gets created right -in the Inspector; `` clears the reference. +| Creation in code | Selection in the Inspector | +|---|---| +|
[SerializeReference]
private IWeapon _primary = new Pistol();
|
[TypeSelector]
[SerializeReference]
private IWeapon _primary;
| -```csharp -using System; -using UnityEngine; -using System.Collections.Generic; -using Aspid.FastTools.Types; +The selector stores an **instance with data**. To store only a class name and create the object later from code, use [Serializable Type System](02-serializable-types.md). -public interface IWeapon -{ - void Fire(); -} +![Switching from Pistol to Shotgun preserves Damage = 37 and adds Pellets](Images/aspid_fasttools_serialize_reference_selector.gif) + +Switching from Pistol to Shotgun preserves Damage = 37 and adds Pellets + +A ready-made scene with weapons, effects, and nested modifiers is included in the [SerializeReferences sample](../Samples~/SerializeReferences/Documentation/README.md). + +## Configuring selection + +The field type sets the base compatibility: `IWeapon` offers its implementations, while an abstract class offers concrete subclasses. Apply `[Serializable]` to classes whose data Unity should persist. + +| Task | Configuration | +|---|---| +| Offer only melee weapons | `[TypeSelector(typeof(IMelee))]` on an `IWeapon` field: candidates must fit the field and implement `IMelee` | +| Warn when a field is empty | `[TypeSelector(Required = true)]` | +| Drive constraints from another field | `[TypeSelector(nameof(_category))]`; see [dynamic constraints](02-serializable-types.md#dynamic-base-types-via-member-references) | +| Change the name, group, tooltip, or icon | `[TypeSelectorDisplay(...)]` on the class | +| Hide an implementation from normal selection | `[TypeSelectorDisplay(Hidden = true)]` | + +`TypeSelector.Allow` is ignored on `[SerializeReference]`: the selector instantiates concrete classes. Interfaces, abstract classes, structs, `string`, delegates, and `UnityEngine.Object` subclasses cannot be the created value. + +### Display name and group +Add the attribute to `Shotgun` from the example: + +```csharp [Serializable] -public sealed class Pistol : IWeapon +[TypeSelectorDisplay( + Name = "Shotgun", + Group = "Weapons/Ranged", + Tooltip = "A weapon that fires multiple pellets")] +public sealed class Shotgun : IWeapon { - [SerializeField] [Min(0)] private int _damage = 10; + [SerializeField, Min(0)] private int _damage = 20; + [SerializeField, Min(1)] private int _pellets = 6; - public void Fire() => Debug.Log($"Pistol: {_damage} dmg"); + public void Fire() => Debug.Log($"Shotgun: {_damage} dmg, {_pellets} pellets"); } +``` + +The class appears under **Weapons → Ranged → Shotgun**. Search still matches its real name, `Shotgun`. These labels do not rename the stored type. + +`Hidden = true` hides a type from normal selection, but existing values keep rendering and assignment from code remains available. Subclasses do not inherit the setting. See [TypeSelectorDisplay](02-serializable-types.md#typeselectordisplay) for all parameters. + +### Required fields + +```csharp +[TypeSelector(Required = true)] +[SerializeReference] private IWeapon _primary; +``` + +An empty field shows **Required reference is not set**. The attribute does not create a value or prevent choosing ``; it also does not replace runtime `null` checks. A missing type is diagnosed separately from an unset field. + +To check required fields in CI, enable [`-srGateRequired`](04-serialize-reference-tooling.md#running-in-ci). The normal pre-build check looks for missing types; the scope of `Required` checks is documented in [SerializeReference Tooling](04-serialize-reference-tooling.md#where-required-fields-are-checked). + +## Lists and nested references -public sealed class Loadout : MonoBehaviour +For arrays and lists, apply both attributes to the collection field. One list can contain different implementations and `null` entries. + +```csharp +// Also import: using System.Collections.Generic; + +[TypeSelector] +[SerializeReference] private List _sidearms = new(); + +[TypeSelector] +[SerializeReference] private IWeapon[] _slots = new IWeapon[2]; +``` + +In a UI Toolkit list, **+** opens the type picker and appends a new instance. Choosing `` appends an empty entry. For the same behaviour in a custom IMGUI Inspector, use `SerializeReferenceIMGUIList.Draw` — see the [example below](#custom-imgui-inspectors). + +### Nested selectors without repeated attributes + +An inner `[SerializeReference]` field gets a selector automatically. For example, add a weapon that wraps another weapon: + +```csharp +[Serializable] +public sealed class DoubleShot : IWeapon { - [TypeSelector] - [SerializeReference] private IWeapon _primary; + [SerializeReference] public IWeapon Weapon; - [TypeSelector] - [SerializeReference] private List _sidearms; + public void Fire() + { + Weapon?.Fire(); + Weapon?.Fire(); + } } ``` -The attribute is editor-only (`[Conditional("UNITY_EDITOR")]`) and carries no runtime -cost. It works with single fields, arrays and `List`, in both IMGUI and UIToolkit -inspectors. The same attribute also works on `string` and `SerializableType` fields — -see [TypeSelectorAttribute](02-serializable-types.md#typeselectorattribute). +Choose **DoubleShot** in `Primary`, then **Pistol** in its **Weapon** field. You do not need to repeat `[TypeSelector]` on `Weapon`. Nested arrays and lists of managed references work the same way. + +Automatic drawing covers eight nesting levels, after which Unity's standard drawing takes over. This is a drawing limit, not a restriction on storing deeper graphs. A child field with its own `[TypeSelector]` or `[CustomPropertyDrawer]` keeps that drawer. + +## Working with data -![Picking an implementation into a managed reference: the picker and the nested inspector of the chosen instance](Images/aspid_fasttools_serialize_reference_selector.gif) +### What happens when switching types -| Feature | What it does | +The selector creates an instance of the selected class and attempts to carry data over from the previous value. For the quick-start example: + +| Field | Pistol before switching | Shotgun after switching | +|---|---|---| +| `_damage` | `37` | `37`: matching name and data shape | +| `_pellets` | Not present | `6`: the new instance's initial value | + +Transfer targets compatible serialized fields. Renamed fields and incompatible data structures need a separate migration. Fields absent from the new type are not retained for later: set **Pellets = 12**, switch to `Pistol`, then back to `Shotgun`, and **Pellets** becomes `6`. + +Nested `[SerializeReference]` fields with matching names and compatible types retain their existing instances. Switching the outer type does not make those references independent copies. + +
+Initial values and constructors + +Creation calls the parameterless constructor, including a non-public one. If there is none, the instance is created without running a constructor, so field initializers cannot be relied on. Keep a parameterless constructor for predictable initial values. + +
+ +### Copy / Paste and templates + +Right-click the **reference field's header** to open its context menu. + +| Action | Result | |---|---| -| **Implementation picking** | The list shows concrete non-`UnityEngine.Object` classes compatible with the field type. `[TypeSelector(typeof(IMelee))]` narrows it to `IMelee` implementations, and `[TypeSelectorDisplay(Hidden = true)]` keeps an individual type out of the picker. | -| **Open generics** | `Modifier` and friends: arguments are inferred from the field — including through the interfaces it implements, so an `IConverter` field closes a `Sequence : IConverter` candidate directly — or picked on the selector's second page when the field leaves a parameter undetermined. A determined candidate is listed closed (`Sequence`), so the row names what the pick will create. A candidate no argument can close to the field is not listed at all — `ToString : IConverter` is absent from an `IConverter` field — while declared variance is honoured, so it stays for an `IConverter` one. An argument only has to be Unity-serializable where the candidate stores it — one keeping `T` behind `[SerializeReference]` closes over any `T` — while the argument page keeps offering serializable types only. | -| **Nested references** | A `[SerializeReference]` field (or array/list) *inside* the assigned instance gets the same dropdown, so a graph can be authored all the way down without annotating every level — 8 levels deep, past which Unity's own drawing resumes. A child Unity already has a drawer for (its own `[TypeSelector]`, or a `[CustomPropertyDrawer]` registered for its type) keeps that drawer. | -| **Data preservation** | On a type switch, fields matching by name and serialized shape carry over instead of resetting to defaults. | -| **Copy / Paste** | Right-clicking the header copies the value and pastes it as an independent instance into any compatible field. | -| **Multi-selection** | A mixed selection shows a mixed dropdown state; a pick or paste applies to every object in a single Undo group. | -| **Compiler validation** | Roslyn analyzer: `AFT0004` (error) — the type inherits `UnityEngine.Object`; `AFT0005` (warning) — the selector would be empty. | - -An empty field with `[TypeSelector(Required = true)]` shows a "required" notice in the -Inspector and counts as a violation for the -[build/CI gate](04-serialize-reference-tooling.md#project-settings--the-buildci-gate) — -see the `Required` property on [TypeSelectorAttribute](02-serializable-types.md#typeselectorattribute). - -## Repairing broken references - -When an asset's stored type stops resolving, or two fields silently share one instance, -the selector does not stay quiet — every problem gets an Inspector notice with a repair -button next to it: - -| Case | Fix | +| **Copy Serialize Reference** | Stores the current value's type and serializable data | +| **Paste Serialize Reference** | Creates a new instance in a compatible field, respecting its type and additional constraints | +| **Save as Template…** | Saves the current value under a name | +| **Paste Template → name** | Creates an instance from a compatible saved template | + +Copying an empty reference is meaningful: the next paste clears the destination. With multiple objects selected, Copy reads the first object's value; selection and Paste create an independent instance per object in one Undo group. A type switch carries data over from each object's own previous value. Check `Required`, `Missing type`, and `Shared reference` notices with a single object selected. + +> [!NOTE] +> The clipboard and templates transfer data through `JsonUtility`; they do not copy an entire nested `[SerializeReference]` graph. To separate a shared reference together with its nested managed references, use **Make unique**. + +Templates are stored locally in `EditorPrefs` for the current project. They are personal presets and are not shared with the team through Git. Saving under an existing name asks for overwrite confirmation. + +### Other header actions + +- **Drag a `.cs` file from Project** to assign an instance of its compatible script class. Data transfers follow the same rules as type selection. +- **Find Usages of …** searches for uses of the current type in the project. +- **Create New Script…** saves a serializable class stub compatible with the declared field type. After successful compilation, the selector assigns a new instance. Add your own logic to the stub: interface methods may contain `NotImplementedException`, and abstract base-class members need manual implementation. + +## Shared references and Make unique + +Two fields on the same component or `ScriptableObject` can point to one instance. Editing its data through either field affects both; the selector labels this **Shared reference**. Sharing may be intentional. + +To create a shared reference, open the destination field's context menu and choose **Link to Existing → type and path**. It offers references compatible with the field type within the same host object. This links an existing instance and replaces the destination's previous value. + +![Make unique creates an independent copy of a shared reference](Images/aspid_fasttools_serialize_reference_make_unique.png) + +Make unique creates an independent copy of a shared reference + +Click **Make unique** in the notice or **Make Unique Reference** in the context menu to edit the field independently. Nested managed references are copied too; repeated references within the copy retain their internal sharing. + +Automatic splitting after duplicating list entries is controlled by **Auto de-alias duplicated list elements** in [FastTools settings](04-serialize-reference-tooling.md#pre-build-checks). It is enabled by default. + +## Generic types + +The selector infers generic arguments from the field type where possible. If some arguments remain unknown, the window offers them on the next page. + +```csharp +public interface IModifier { } + +[Serializable] +public class Modifier : IModifier +{ + public T Value; +} + +// T is known: creates Modifier. +[TypeSelector] +[SerializeReference] private Modifier _damageModifier; + +// Modifier asks you to choose T in the selector. +[TypeSelector] +[SerializeReference] private IModifier _modifier; +``` + +Declare the interface and class alongside the other types, and add the fields to `Loadout`. The first field fixes the argument to `float`; for the second, choose an argument such as `int` or `string` on the argument page. + +
+Inference through interfaces and argument constraints + +Arguments are also inferred through implemented interfaces: an `IConverter` field closes `Sequence : IConverter` as `Sequence`. + +A candidate is excluded if it cannot be closed to fit the field. For example, `ToString : IConverter` does not fit `IConverter`. If the output parameter of `IConverter` is covariant, it can fit `IConverter`. + +An inferred argument must support by-value serialization only where the candidate stores it by value. A parameter behind `[SerializeReference]` follows managed-reference rules. The manual argument page offers serializable types. + +
+ + + +## Repairing missing types + +Renaming, moving, or deleting a class can leave its stored name unresolved. The field shows **Missing type**. While the reference's data remains in the asset, it can be reassigned to an existing implementation. + +![A missing reference with Fix and Smart Fix actions in the Inspector](Images/aspid_fasttools_serialize_reference_repair.png) + +A missing reference with Fix and Smart Fix actions in the Inspector + +| Action | When to use it | |---|---| -| **Missing type** (renamed or deleted) | A yellow notice instead of a silent clear. The underlined **Fix** opens the picker and re-points the type while keeping its data — at any depth, in saved assets and live in Prefab Mode. | -| **Smart Fix** | Next to **Fix**, suggests the most likely replacement (`[MovedFrom]`, a different namespace/assembly, casing, a near-miss name) and applies it in one click — never automatically. | -| **Shared reference** (two fields share one instance) | Flagged with a notice; **Make unique** splits it into an independent copy. Duplicating a list element (Ctrl+D, `+`) no longer aliases the reference. | +| **Fix** | You know a suitable replacement: open the picker and select an existing type | +| **Smart Fix** | You want to use the suggested replacement: check the type and reason in the tooltip, then click the suggestion | + +Smart Fix considers `[MovedFrom]`, the name, namespace, assembly, and field similarity. It only applies when clicked. The **Fix** picker also permits `Hidden` types: recovering old data may require an implementation removed from normal selection. + +For an asset on disk, Fix rewrites the stored type and reimports the asset; that file write has no ordinary Undo. In an open saved scene or Prefab Mode, repair affects the object in memory — verify the result and save the scene or prefab. Preserving data does not automatically convert incompatible fields; in-memory repair also does not guarantee recovery of the entire nested graph. + +If Fix is unavailable, select one object and ensure the scene or Prefab Mode is saved with no pending changes. For a prefab instance in a scene, open its source prefab. If the problem is inside a missing parent and the field is inaccessible, use [Asset References](04-serialize-reference-tooling.md#asset-references-inspect-one-asset). + +Accompany planned renames with [`[MovedFrom]`](04-serialize-reference-tooling.md#migrations-with-movedfrom). For auditing and repairing multiple assets, see [SerializeReference Tooling](04-serialize-reference-tooling.md). + +## Custom IMGUI inspectors + +The selector works in IMGUI and UI Toolkit. In a custom IMGUI editor, a regular `PropertyField` uses the field's drawer, but a list's **+** button needs `SerializeReferenceIMGUIList.Draw` to open a type picker. + +For `Loadout` with the `_sidearms` field above, put this editor in an `Editor` folder: + +```csharp +using UnityEditor; +using UnityEngine; +using Aspid.FastTools.SerializeReferences.Editors; + +[CustomEditor(typeof(Loadout))] +public sealed class LoadoutEditor : Editor +{ + public override void OnInspectorGUI() + { + serializedObject.Update(); + + EditorGUILayout.PropertyField( + serializedObject.FindProperty("_primary"), true); + + SerializeReferenceIMGUIList.Draw( + serializedObject.FindProperty("_sidearms"), + new GUIContent("Sidearms"), + typeof(IWeapon)); + + serializedObject.ApplyModifiedProperties(); + } +} +``` + +Add any other fields to the editor as needed. To create controls without `[TypeSelector]`, use `SerializeReferenceEditorGUI.CreateField`, `CreateList`, or `DrawFieldLayout`; a complete editor is included in the [SerializeReferences sample](../Samples~/SerializeReferences/Documentation/README.md#the-imgui-path). -![Missing-type notice with the Fix and Smart Fix actions on a broken managed reference](Images/aspid_fasttools_serialize_reference_repair.png) +## If a type is missing from the list -![Shared-reference notice with the Make unique action on two fields aliasing one instance](Images/aspid_fasttools_serialize_reference_make_unique.png) +Check that the class is concrete, compatible with the field and its additional constraints, does not inherit `UnityEngine.Object`, and is not marked `Hidden = true`. A generic candidate must have valid arguments. After compilation errors, wait for scripts to compile successfully. -For auditing and mass repair across the whole project, see -[Bulk repair tabs](04-serialize-reference-tooling.md#bulk-repair-tabs). +Analyzer `AFT0004` reports incompatibility with `UnityEngine.Object`; `AFT0005` warns about a potentially empty selector. `Allow` does not broaden the set of instantiable managed references. +`[TypeSelector]` and `[TypeSelectorDisplay]` attributes apply only in the editor. Implementations and their serialized data remain part of the game. diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/04-serialize-reference-tooling.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/04-serialize-reference-tooling.md index 82b43f55..91d4ec9f 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/04-serialize-reference-tooling.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/04-serialize-reference-tooling.md @@ -1,79 +1,227 @@ # SerializeReference Tooling -The [Inspector-side selector](03-serialize-reference-selector.md) repairs references one field at a -time; this document covers the project-wide side: the FastTools window tabs that audit -and mass-repair managed references, the Project Settings page with the player-build gate, -and the same check running headless in CI. The gate also covers unset -`[TypeSelector(Required = true)]` fields — see the `Required` property on -[TypeSelectorAttribute](02-serializable-types.md#typeselectorattribute). +Find missing `[SerializeReference]` types in prefabs, assets, and scenes, repair them in groups, and check the project before building. FastTools reads stored references from YAML, including entries the ordinary Inspector no longer shows. -**Reference sections:** + -* [`Bulk repair tabs`](#bulk-repair-tabs) — the **Asset References** and - **Project References** tabs for auditing and mass repair across the project; -* [`Project settings & the build/CI gate`](#project-settings--the-buildci-gate) — - the Project Settings page, setting scopes and the player-build gate; -* [`Headless CI`](#headless-ci) — `SerializeReferenceCiGate.RunCheck` for batchmode pipelines. +## Quick start -## Bulk repair tabs +After renaming or deleting a class, check which assets still store its old name: -There is no need to [fix references one by one](03-serialize-reference-selector.md#repairing-broken-references): -auditing and mass repair live in two dedicated tabs of the FastTools window. +1. Save modified scenes and assets — scanning reads files on disk. +2. Open **Tools → Aspid 🐍 → FastTools → Project References**. +3. Click **Scan Project**. Missing references are grouped by stored type. +4. Click **Fix all** in the relevant group, choose a replacement, and review the changes in the **Rewrite** dialog. +5. Review the summary and the affected assets' values. If needed, use **Undo** in the operation summary; then click **Rescan** to check again. -| Tab | Purpose | +File rewrites skip references in open scenes and Prefab Mode. Save and close those scenes or prefabs before repair, or repair a visible field using [Fix in the Inspector](03-serialize-reference-selector.md#repairing-missing-types). + +> [!NOTE] +> Analysis requires text YAML assets. In Unity's editor settings, select **Asset Serialization → Mode → Force Text**. Existing binary assets need to be saved again; changing the mode alone does not make them scannable. + + + +## Audit and repair windows + +| Task | Tool | +|---|---| +| Repair one visible field | [Inspector selector](03-serialize-reference-selector.md#repairing-missing-types) | +| Inspect connections within one saved asset | **Asset References** | +| Find a missing type throughout the project | **Project References** | +| Write the new type name after `[MovedFrom]` | **Migrate all** in Project References | +| Validate data before building | **Build / CI gate** in project settings | + +Both tabs open through **Tools → Aspid 🐍 → FastTools**. Project scanning processes `.prefab`, `.asset`, and `.unity` files under `Assets/`, respecting **Excluded scan folders**. It covers eligible project files, not just scenes included in the build. + +## Project References: repair a group + +Each group card shows the stored type and reference and file counts. Asset paths and reference IDs (`rid`) appear below. Click a row to open its asset in **Asset References**: + +![A missing-reference group with Fix all and Smart Fix actions](Images/aspid_fasttools_serialize_reference_project_references.png) + +A missing-reference group with Fix all and Smart Fix actions + +### Choosing an action + +| Action | Behaviour | +|---|---| +| **Fix all** | Opens the type picker and applies the replacement to all writable references in the group | +| **Smart Fix** | Uses the suggested type and opens replacement confirmation | +| **Migrate all** | Rewrites the old name to the type uniquely identified through `[MovedFrom]` | +| **Reassign all** | Lets you choose a different replacement for a group recognized as a migration | + +**Smart Fix** appears when type information, name similarity, and fields identify a suitable candidate. Review the suggestion; scanning alone does not repair anything. + +### What repair preserves + +Replacing a missing type changes its `class`, `ns`, and `asm` entry in YAML while retaining the data block and `rid`. Unity then reimports the file and reads the data as the new type. + +Choose a type compatible with both the declared field type and the stored data. Renaming does not automatically transform the field layout. If a group spans different field types, the dialog warns you: the chosen class may not fit every entry, and incompatible references become `null` on import. + +Bulk replacement produces a summary with an **Undo** button. It restores the old type name on references that still contain the applied replacement. This summary action does not restore every previous asset value. Review the result before scanning again: **Rescan** clears earlier operation summaries. + +### Choosing None + +`` clears references and deletes their stored data. If several fields share a `rid`, all pointers to that instance are cleared. The tool asks for confirmation; this operation cannot be undone. + +Bulk clearing may null references in open scenes or Prefab Mode in memory. Save those objects: file-based scans continue to show the old entries until they are saved. + +## Asset References: inspect one asset + +Open **Asset References** and assign a saved prefab, ScriptableObject, or scene file to the object field beside **Rescan**. You can also arrive here from a **Project References** result row. + +The graph groups references by host object and field path: + +| Label | Meaning | +|---|---| +| **MISSING** | The reference's stored type cannot be found | +| **SHARED** | Several fields use the same managed-reference instance | +| **Orphaned** | A YAML entry remains with no field pointing to it | +| `rid` | A managed-reference identifier within its host object | + +`SHARED` does not inherently mean an error: sharing can be intentional. Matching colours help locate connected fields; the colour is derived from the ID and has no separate setting. + +Open **Fix** on a missing-reference card and choose a replacement. In this example, `GhostWeapon` becomes `Pistol`: + +![Repairing GhostWeapon as Pistol while preserving reference data](Images/aspid_fasttools_serialize_reference_tooling.gif) + +Repairing GhostWeapon as Pistol while preserving reference data + +The scene or prefab must be closed for a YAML rewrite. If a regular field cannot be edited from this window — for example, it is in a scene or beneath a missing parent reference — repair the parent or open the field in the Inspector. + +Orphaned entries offer **Clear**. This deletes the file entry after confirmation and does not support Undo. + +## Migrations with MovedFrom + +For an intentional rename or move, `[MovedFrom]` connects the old identity to the new type. For example, renaming `GhostWeapon` to `Pistol` within the same assembly and namespace: + +| Before — GhostWeapon | After — Pistol | |---|---| -| **Asset References** (`Tools → Aspid 🐍 → FastTools → Asset References`) | Maps an asset's whole managed-reference graph from its YAML — a per-component tree with field paths, shared and orphaned references, `MISSING` / `SHARED` badges, and an inline type dropdown on every card. Surfaces the missing references the Inspector cannot show. | -| **Project References** (`Tools → Aspid 🐍 → FastTools → Project References`) | `Scan Project` sweeps every `.prefab` / `.asset` / `.unity` under `Assets/`, groups broken references by stored type, and rewrites a whole group with a single `Fix all` (plus Smart Fix). A group whose stored type matches a declared `[MovedFrom]` rename reads as a pending migration instead of a breakage — one **Migrate all** click bakes the rename into the files, after which the attribute can be removed from code. | +|
[Serializable]
public sealed class GhostWeapon
{
    public int Damage = 10;
}
|
[Serializable]
[MovedFrom(true,
    sourceClassName: "GhostWeapon")]
public sealed class Pistol
{
    public int Damage = 10;
}
| + +The attributes require `using System;` and `using UnityEngine.Scripting.APIUpdating;`. For moves, also supply the old `sourceNamespace` and `sourceAssembly`. -The **Asset References** tab lays out one asset's managed-reference graph as cards with -`MISSING` / `SHARED` badges and inline repair: +After compilation: -![Asset References tab: an asset's reference graph with a Fix Missing card](Images/aspid_fasttools_serialize_reference_asset_references.png) +1. Click **Scan Project** or **Rescan**. +2. If the old identity uniquely maps to a suitable type, the group appears as a pending migration. +3. Click **Migrate all** to write the new name to the files. Until then, Unity uses the attribute when loading the old name. -The **Project References** tab groups the whole project's findings by stored type — one -group is repaired at once with a single `Fix all`: +A pending migration does not count as a missing type for build checks. If multiple types claim one old identity, the tool does not automatically choose a winner. Stored closed generic types are not recognized as unambiguous migrations by this mechanism either. -![Project References tab: a group of broken references with Fix all and Smart Fix](Images/aspid_fasttools_serialize_reference_project_references.png) +Remove `[MovedFrom]` only after migrating all data that must remain loadable, including assets outside the current project and folders excluded from scanning. -## Project settings & the build/CI gate + -**`Project Settings → Aspid FastTools → SerializeReference`** exposes: +## Pre-build checks -| Setting | Scope | What it does | +Open **Project Settings → Aspid FastTools → SerializeReference** and set **Build / CI gate**: + +| Mode | Player build | Standalone CI run | |---|---|---| -| **Breakage detection** | per-user | The proactive toast + console warning when references newly become missing after a recompile / import. | -| **Auto de-alias duplicated list elements** | committed | A duplicated list element gets its own instance instead of sharing the original's reference id. | -| **Build / CI gate** | committed | `Off` / `Warn` / `Fail`: at player-build time, log or abort on missing (and, for CI, unset-required) managed references. | -| **Excluded scan folders** | committed | Paths skipped by every project scan. | +| `Off` | Skips validation | Skips scanning and report writing; exit code `0` | +| `Warn` | Warns and continues building | Logs violations; exit code `0` | +| `Fail` | Missing types stop the build | Exit code `1` when violations are found | -- Committed values live in `ProjectSettings/SerializeReferenceSharedSettings.asset` — commit it so teammates and CI behave identically; breakage detection stays per-machine (`EditorPrefs`). -- Rid colours are not a setting — a shared reference is always colour-coded by id, so matching colours reveal shared instances at a glance. +The default is `Warn`. This setting controls validation; it does not repair references. -The same options are mirrored in the window's **Settings** tab (`Tools → Aspid 🐍 → FastTools → Settings`) and at **`Preferences → Aspid FastTools`**, alongside the picker's per-user preferences: +### Where required fields are checked -- **Favorites** — section on/off toggle. -- **Recent items** — capacity slider (0–20; 0 hides the section and pauses recording without wiping history). -- **Saved lists** — clears the stored Favorites / Recent. -- **Welcome** — auto-show toggle. +| Run | Missing types | Empty fields with `TypeSelector(Required = true)` | +|---|---|---| +| **Project References → Scan Project** | Yes, including pending-migration groups | Yes in `Warn` or `Fail`; a separate **Required violations** group | +| Player build | Yes in `Warn` or `Fail` | No | +| CI without `-srGateRequired` | Yes, when enabled | No | +| CI with `-srGateRequired` | Yes, when enabled | Yes | + +Required checks follow the traversal limits in [Running in CI](#running-in-ci). Project scanning for missing types is available even in `Off`; only the additional Required check is disabled. + +### Shared and personal settings + +| Setting | Storage | Purpose | +|---|---|---| +| **Build / CI gate** | Project | Validation severity | +| **Excluded scan folders** | Project | Folders skipped by project scans | +| **Auto de-alias duplicated list elements** | Project | Creates an independent copy when duplicating a list entry | +| **Breakage detection** | Local `EditorPrefs` | A notification and Console warning for newly missing references after import or recompilation | -Every row carries a scope stripe (green — committed, blue — per-user); a pinned footer offers **Reset to defaults** per scope (saved Favorites / Recent lists survive a reset). All surfaces stay in live sync. +Shared settings are saved in `ProjectSettings/SerializeReferenceSharedSettings.asset`. Commit this file so the team and CI use the same rules. -## Headless CI +
+Other window and selector settings -For headless CI, the same check runs via `SerializeReferenceCiGate.RunCheck`: it scans -the project, writes a report, logs every violation, and honours the committed gate -severity — `Off` skips the check, `Warn` logs but exits 0, `Fail` exits 1 when -violations exist (exit code 2 marks an internal failure of the check itself). +The same options are available in the FastTools window's **Settings** tab and **Preferences → Aspid FastTools**. Personal settings are nearby: + +- **Favorites** — shows or hides favourites. +- **Recent items** — history capacity from 0 to 20. Setting 0 hides the section and pauses recording while retaining history. +- **Saved lists** — clears Favorites and Recent. +- **Welcome** — shows the welcome screen automatically. + +A green stripe marks project settings; blue marks personal settings. **Reset to defaults** resets each group separately and preserves Favorites and Recent. Changes immediately appear in all settings views. + +
+ + + +## Running in CI + +Run the Unity Editor from the Unity project root. `Unity` stands for the editor executable; supply its full path if it is not on `PATH`. ```bash Unity -batchmode -quit -projectPath . \ -executeMethod Aspid.FastTools.SerializeReferences.Editors.SerializeReferenceCiGate.RunCheck \ - -srGateReport SerializeReferenceGateReport.txt -srGateRequired + -srGateReport SerializeReferenceGateReport.txt \ + -srGateRequired -srGateFail ``` -| Flag | Description | +This checks missing types and unset required fields, writes a report, and exits with code `1` on violations. `-srGateFail` explicitly enables strict mode even if the project uses `Off`. Running the check does not repair assets. + +### Command-line flags + +| Flag | Behaviour | |---|---| -| `-srGateReport ` | Report file path; defaults to `SerializeReferenceGateReport.txt` in the project root. Each violation is a machine-readable line with the violation kind, asset path and field path. | -| `-srGateRequired` | Also flags unset `[TypeSelector(Required = true)]` fields across prefabs, ScriptableObjects and scenes (top-level fields, pure-YAML pass). | -| `-srGateWarnOnly` | Overrides the committed severity to `Warn` for this run: violations are logged but the exit code is 0. Wins over `-srGateFail` if both are passed. | -| `-srGateFail` | Overrides the committed severity to `Fail` for this run: exit code 1 when violations exist. | +| `-srGateReport ` | Report path; defaults to `SerializeReferenceGateReport.txt` | +| `-srGateRequired` | Also checks unset fields with `Required = true` | +| `-srGateFail` | Uses `Fail` instead of the project setting | +| `-srGateWarnOnly` | Uses `Warn`; takes precedence over `-srGateFail` if both are passed | + +Without a severity flag, the project setting applies. For a trial run, replace `-srGateFail` with `-srGateWarnOnly`. + +In `Warn`, violations are logged as errors, but the process returns `0`. Check the exit code in CI. In `Off`, no fresh report is written; a report from an earlier run may remain on disk. + +### Required check boundaries + +For prefabs and ScriptableObjects, validation traverses serialized properties, including accessible nested fields. Scenes are read from YAML: top-level fields and fields within by-value containers are checked. Scene traversal does not descend into collection entries or managed references. + +This limitation applies to unset required fields. Missing-type detection separately reads stored managed-reference entries. + +### Report and exit codes + +After the header, each violation occupies one line. Fields are tab-separated: + +```text +KIND assetPath fileId rid className fieldPath +``` + +| Field | Contents | +|---|---| +| `KIND` | `MissingType` or `RequiredUnset` | +| `assetPath` | File path, such as `Assets/Weapons/Pistol.prefab` | +| `fileId` | Host object ID within the file | +| `rid` | Managed-reference ID; `0` for a required string field | +| `className` | Stored class name for `MissingType`, without separate namespace or assembly fields | +| `fieldPath` | Required field path; empty for `MissingType` | + +Save the report as a CI artifact. The asset path, `fileId`, and `rid` together help locate the entry in Asset References. + +| Code | Meaning | +|---|---| +| `0` | No violations, `Warn` selected, or validation disabled | +| `1` | Violations found in `Fail` mode | +| `2` | An internal check error, such as failure to write the report | + +## Next steps + +- [SerializeReference Selector](03-serialize-reference-selector.md) — type selection, shared references, and individual field repair in the Inspector. +- [Serializable Types](02-serializable-types.md) — `TypeSelector` and required-field configuration. +- [SerializeReferences sample](../Samples~/SerializeReferences/Documentation/README.md) — polymorphic weapon and effect data in a working scene. diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/05-profiler-markers.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/05-profiler-markers.md index 89c35067..c72074ae 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/05-profiler-markers.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/05-profiler-markers.md @@ -1,59 +1,94 @@ # ProfilerMarkers -Provides source-generated `ProfilerMarker` registration. The generator creates a static marker per call-site, identified by the calling method and line number. +`this.Marker()` creates a Unity Profiler marker named `Type.Method (line)` — no static `ProfilerMarker` field and no hand-typed name. -```csharp -using UnityEngine; +## Quick start + +The examples on this page work with the `FlockSimulation` class from the [ProfilerMarkers sample](../Samples~/ProfilerMarkers/Documentation/README.md): + +| Before — Unity API | After — FastTools | +|---|---| +|
private static readonly
    ProfilerMarker StepMarker =
    new("FlockSimulation.Step");

public void Step()
{
    using var _ = StepMarker.Auto();
    Integrate();
}
|
public void Step()
{
    using var _ = this.Marker();
    Integrate();
}
| + +Works in `MonoBehaviour` and ordinary C# classes. The generator ships with the package; the extension is in the global namespace — no extra `using`, attributes, or `partial` declaration needed. + +## Marker() + +Returns the `ProfilerMarker.AutoScope` of the `Type.Method (line)` marker for the current call site. -public class MyBehaviour : MonoBehaviour +> [!IMPORTANT] +> Do not call `this.Marker()` without `using`: the measurement will not end automatically. A scope must not cross `await` or `yield`; measure synchronous sections separately ([Unity limitation](https://docs.unity3d.com/6000.0/Documentation/Manual/profiler-add-markers-code.html)). + +## WithName() + +`.WithName("Steering")` replaces the method part of the name; the type and line number remain. + +```csharp +public void Step() { - private void DoSomething1() - { - using var _ = this.Marker(); - // Some code - } + using var _ = this.Marker(); - private void DoSomething2() + using (this.Marker().WithName("Steering")) { - using (this.Marker()) + foreach (var agent in _agents) { - // Some code - using var _ = this.Marker().WithName("Calculate"); - // Some code + using var agentScope = this.Marker().WithName("Steering.Agent"); + ComputeSteering(agent); } } + + using (this.Marker().WithName("Integrate")) + { + Integrate(); + } } ``` -## Generated code +
+Generated code -```csharp -using Unity.Profiling; -using System.Runtime.CompilerServices; +Abridged: without `global::` and the repeated attribute. Line numbers count from the top of the block above; in a real file they are source-file lines. -internal static class __MyBehaviourProfilerMarkerExtensions +```csharp +// +[GeneratedCode("Aspid.FastTools.Generators.ProfilerMarkersGenerator", "1.0.0")] +internal static class __FlockSimulationProfilerMarkerExtensions { - private static readonly ProfilerMarker DoSomething1_Marker_Line_7 = new("MyBehaviour.DoSomething1 (7)"); - private static readonly ProfilerMarker DoSomething2_Marker_Line_13 = new("MyBehaviour.DoSomething2 (13)"); - private static readonly ProfilerMarker DoSomething2_Marker_Line_16 = new("MyBehaviour.Calculate (16)"); + private static readonly ProfilerMarker Step = new("FlockSimulation.Step (3)"); + private static readonly ProfilerMarker Step_2 = new("FlockSimulation.Steering (5)"); + private static readonly ProfilerMarker Step_3 = new("FlockSimulation.Steering.Agent (9)"); + private static readonly ProfilerMarker Step_4 = new("FlockSimulation.Integrate (14)"); - public static ProfilerMarker.AutoScope Marker(this MyBehaviour _, [CallerLineNumberAttribute] int line = -1) + public static ProfilerMarker.AutoScope Marker(this FlockSimulation _, [CallerLineNumber] int line = -1) { #if ENABLE_PROFILER - if (line is 7) return DoSomething1_Marker_Line_7.Auto(); - if (line is 13) return DoSomething2_Marker_Line_13.Auto(); - if (line is 16) return DoSomething2_Marker_Line_16.Auto(); + if (line is 3) return Step.Auto(); + if (line is 5) return Step_2.Auto(); + if (line is 9) return Step_3.Auto(); + if (line is 14) return Step_4.Auto(); #endif return default; } } ``` -The dispatcher body is wrapped in `#if ENABLE_PROFILER`: in a build without the profiler every call returns `default` and costs nothing. +
+ +The tree in **CPU Usage → Hierarchy** mirrors the `using` nesting, and a marker inside a loop stays a single row with a `Calls` count. Deep Profile is not needed. + +![FlockSimulation marker diagram: Steering and Integrate nested under Step, Steering.Agent with 120 calls. Timings are illustrative.](Images/profiler-markers-hierarchy.svg) + +FlockSimulation marker diagram: Steering and Integrate nested under Step, Steering.Agent with 120 calls. Timings are illustrative. + +`WithName` accepts only a string literal: `"Steering"`, `@"Steering"`, or `$"Steering"` without substitutions. Variables, `const`, `nameof`, concatenation, and `$"Agent {index}"` keep the original method name — the generator reads the source text and does not evaluate expressions. The argument is still evaluated at runtime. + +## Generation details -- **Marker name** — `"{TypeName}.{method} ({line})"`; `.WithName("…")` replaces the member part. For generic enclosing types the name is built with `typeof(T).Name`, so each closed type gets its own marker. -- **Call sites inside lambdas and local functions** resolve to the nearest declared method, field or property. +- **Line number.** Every call in a type needs its own line, including across `partial` files. Moving a call changes the name suffix. +- **Member name.** A method contributes its own name, a constructor `Ctor`, a property accessor the property name. Lambdas and local functions use the member they are declared in. +- **Generic types** get separate markers per closed type: `Worker.Run()` → `Worker.Run (line)`. +- **Without `ENABLE_PROFILER`** nothing is measured, but the code inside `using` and the `WithName` arguments still run. -## Result +## Package sample -![Generated markers in the Unity Profiler window](Images/aspid_fasttools_profiler_markers.png) +A flock scene with 120 agents where `FlockSimulation` carries the markers shown above: [ProfilerMarkers](../Samples~/ProfilerMarkers/Documentation/README.md). diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/06-enum-values.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/06-enum-values.md index 35a682c8..2288eed8 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/06-enum-values.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/06-enum-values.md @@ -1,53 +1,199 @@ # EnumValues -Serializable enum-to-value mappings configurable from the Inspector. +An enum-keyed table configured in the Inspector: damage multipliers, colours, sounds, asset references. `GetValue` returns the matching row's value, or `Default Value` when no row matches. -## EnumValues\ +## Quick start -A serializable collection of `EnumValue` entries with a configurable default value. Implements `IEnumerable>`. +The examples use this enum: -`GetValue` returns the mapped value, falling back to the configured default when the key is missing. `[Flags]` enums are supported: matching uses `HasFlag` and treats `0`-valued members correctly. +```csharp +public enum DamageType +{ + Physical, Fire, Ice, Poison +} +``` + +Add `using Aspid.FastTools.Enums;` to a script that imports `UnityEngine`. One table replaces a set of serialized fields and a `switch`: + +| Before — separate fields and switch | After — EnumValues | +|---|---| +|
[SerializeField]
private float _defaultMultiplier = 1f;
[SerializeField]
private float _fireMultiplier = 1.5f;

public float GetMultiplier(
    DamageType type) => type switch
{
    DamageType.Fire => _fireMultiplier,
    _ => _defaultMultiplier
};
|
[SerializeField]
private EnumValues<DamageType, float>
    _multipliers;

public float GetMultiplier(
    DamageType type) =>
    _multipliers.GetValue(type);
| + +![Fire uses a multiplier of 1.5; other damage types use Default Value 1](Images/enum-values-multipliers-quick-start.png) + +Fire uses a multiplier of 1.5; other damage types use Default Value 1 + +| Call | Result | +|---|---| +| `_multipliers.GetValue(DamageType.Fire)` | `1.5` — the `Fire` row's value | +| `_multipliers.GetValue(DamageType.Ice)` | `1` — no `Ice` row, so `Default Value` is returned | + +## Inspector setup + +1. Expand the table and set **Default Value** — keys without a row of their own receive it. +2. Add rows by hand, or right-click the property and choose **Populate Missing Enum Members**. +3. Configure the values of the added rows. + +Only keys whose value differs from `Default Value` need a row. + +### Populate Missing Enum Members + +Appends the missing enum members to the table with the current `Default Value` as their value. + +![Populate Missing Enum Members adds rows with a value of 1, preserving Fire = 1.5; Undo reverts the operation](Images/enum-values-multipliers-populate.gif) + +Populate Missing Enum Members adds rows with a value of 1, preserving Fire = 1.5; Undo reverts the operation + +For `[Flags]` it adds only declared members, including named combinations. It does not generate every possible bit combination. + +## Choosing a variant + +| Task | Field type | Enum choice in the Inspector | Key in `GetValue` | +|---|---|---|---| +| The enum is known in code | `EnumValues` | Fixed by the `TEnum` argument; the type field is read-only | `TEnum`: checked by the compiler, no boxing to `object` | +| The asset author picks the enum | `EnumValues` | Available in the table header | `System.Enum`: the key is boxed, and a foreign enum compiles and returns `Default Value` | + +Both variants support `Default Value`, `[Flags]` and row enumeration. `TValue` is any type Unity serializes: `float`, `Color`, `AudioClip`, your own `[Serializable]` class. + +### EnumValues\ ```csharp -using System; -using UnityEngine; -using Aspid.FastTools.Enums; +[SerializeField] private EnumValues _multipliers; +``` -public enum DamageType { Physical, Fire, Ice, Poison } +The enum is fixed in code and the compiler checks the key type. The full example is in the [quick start](#quick-start). -[Flags] -public enum StatusEffect { None = 0, Burning = 1, Frozen = 2, Slowed = 4, Stunned = 8 } +### EnumValues\ -public sealed class DamageDealer : MonoBehaviour -{ - [SerializeField] private EnumValues _damageMultipliers; +The same field without `DamageType` in the declaration; the enum is chosen in the Inspector: + +```csharp +[SerializeField] private EnumValues _multipliers; + +public float GetMultiplier(DamageType type) => _multipliers.GetValue(type); +``` + +For this example, select **DamageType** in the table header. A key from another enum returns `Default Value` even when the numeric value happens to match. + +![Open the type selector in the Multipliers header and search for DamageType](Images/enum-values-type-selector.png) + +Open the type selector in the Multipliers header and search for DamageType + +> [!IMPORTANT] +> When no enum is selected, the table returns `Default Value` and logs a warning to the Console on the first access. When the stored type is no longer found in the project, for example after a rename, it logs an error instead. + +## Lookup rules + +The table is scanned top to bottom. For a regular enum the first row with the same numeric key wins, otherwise the result is `Default Value`: - // Flag combinations (e.g. Burning | Slowed) match via HasFlag and first-hit wins, - // so list composite entries BEFORE their constituent flags. - [SerializeField] private EnumValues _speedMultipliersByStatus; +| Situation | Result | +|---|---| +| Key found | The row's value, including `0`, `false` or `null` | +| Key missing or table empty | `Default Value` | +| Several rows with the same numeric key | The first of them | +| Different enum names with the same numeric value | One key for lookup purposes | - public float GetMultiplier(DamageType type) => _damageMultipliers.GetValue(type); +### Flags - public float GetSpeedModifier(StatusEffect effects) => _speedMultipliersByStatus.GetValue(effects); +For `[Flags]`, lookup checks for an exact match before checking flag containment. + +Zero matches only zero; it is not an "empty mask" that matches the other flags. Example: + +```csharp +[Flags] +public enum StatusEffect +{ + None = 0, + Burning = 1, + Slowed = 2, + Frozen = 4 } + +[SerializeField] private EnumValues _speedMultipliers; ``` -![EnumValues in the Inspector](Images/aspid_fasttools_enum_values.png) +`Default Value` is `1` and the rows are in this order: + +| Key | Value | +|---|---| +| `Burning` | `0.9` | +| `Slowed` | `0.5` | +| `Burning \| Slowed` | `0.3` | +| `None` | `1` | + +
    +
  1. + Exact match + Look for the entire requested set of flags. + Burning | Slowed → 0.3 + The exact row wins, even when it is farther down. + No exact row → +
  2. +
  3. + First matching row + All of its flags must be present in the request. + Burning | Frozen → 0.9 + Burning wins; row order matters. + No matching row → +
  4. +
  5. + Default Value + Return the configured fallback. + Frozen → 1 + There is no exact or matching row. +
  6. +
+ +> [!NOTE] +> The second pass takes the first matching row, not the most complete one. For `Burning | Slowed | Frozen` both `Burning` and `Burning | Slowed` match, but `Burning` wins because it sits higher: the result is `0.9`. To let a combination win, place combined rows above single flags. + +## Checking keys with Equals + +`Equals(first, second)` compares keys by the same rules without reading row values. For a regular enum this is numeric equality. For `[Flags]` it checks whether the **first argument contains all bits of the second**; zero equals only zero: -In the Inspector, select the enum type in the `EnumValues` header, then assign a value for each enum member. Right-click the property to open a context menu with **Populate Missing Enum Members** — it appends an entry for every enum member not yet in the list, seeded with the current Default Value. +```csharp +var combined = StatusEffect.Burning | StatusEffect.Slowed; -## EnumValues\ +_speedMultipliers.Equals(combined, StatusEffect.Burning); // true +_speedMultipliers.Equals(StatusEffect.Burning, combined); // false +_speedMultipliers.Equals(combined, StatusEffect.None); // false +_speedMultipliers.Equals(StatusEffect.None, StatusEffect.None); // true +``` + +Use `==` for strict enum equality. In `EnumValues` both arguments must belong to the selected enum, otherwise the result is `false`. -The typed counterpart of `EnumValues` for the common case where the enum type is already known in code. The enum is fixed by the generic argument, so the Inspector's type picker is disabled and lookups are compile-time safe. Lookups are also boxing-free — keys are compared as cached numeric values — and `foreach` over either variant binds to a struct enumerator, so iteration does not allocate. Implements `IEnumerable>`. +## Enumerating rows + +`foreach` yields the configured rows in list order. `Default Value` and rows with an unresolved key are not included: ```csharp -public sealed class HitEffect : MonoBehaviour +foreach (var (type, multiplier) in _multipliers) { - // The type picker in the Inspector is disabled — the enum is fixed to DamageType. - [SerializeField] private EnumValues _damageColors; - - public Color GetColor(DamageType type) => _damageColors.GetValue(type); + Debug.Log($"{type}: {multiplier}"); } ``` -Lookup semantics (including `[Flags]` handling) are identical to `EnumValues`. +The typed table yields `TEnum` keys, the generic one `System.Enum`. A direct `foreach` uses a struct enumerator and does not allocate; enumerating through the `IEnumerable` interface, for example in LINQ, boxes it. + +## Changing the table and enum + +The public API only reads the table: there is no `Add`, `Remove` or writable indexer, and values are set through Unity serialization. + +Keys are stored by member **name**: + +| Enum change | Result | +|---|---| +| Members reordered or their numeric values changed | The table works as before | +| Member added | Returns `Default Value` until a row is added; **Populate Missing Enum Members** fills the gap | +| Member renamed or deleted | Its row is no longer recognised: initialization logs an error to the Console, and lookup and enumeration skip it | + +> [!WARNING] +> As soon as such a row is drawn in the Inspector, its key is silently replaced with the first enum member. Rename members before opening the asset in the Inspector, or review the rows right after. + +## Package sample + +Tiles and footprints take their colour from `EnumValues`, and the speed multiplier from an `EnumValues` with a `[Flags]` enum selected in the Inspector: [EnumValues](../Samples~/EnumValues/Documentation/README.md). + +![The character walks across different surfaces and leaves a continuous coloured trail.](../Samples~/EnumValues/Documentation/Images/demo.gif) + +The character walks across different surfaces and leaves a continuous coloured trail. diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/07-visual-element-extensions.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/07-visual-element-extensions.md index d934f1d1..0db503d0 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/07-visual-element-extensions.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/07-visual-element-extensions.md @@ -1,648 +1,763 @@ # VisualElement Extensions +UI Toolkit extensions for building element trees, setting styles, subscribing to events, and binding editor fields. Methods return the configured element so calls can be chained. + + + +## Quick start + +Add `using Aspid.FastTools.UIElements;` to a script that imports `UnityEngine.UIElements`. These examples create the same panel with a heading: + +| Before — Unity API | After — FastTools | +|---|---| +|
var title = new Label("Stats");
title.style.fontSize = 18;

var panel = new VisualElement();
panel.style.paddingLeft = 12;
panel.style.paddingRight = 12;
panel.style.paddingTop = 8;
panel.style.paddingBottom = 8;
panel.Add(title);
|
var panel = new VisualElement()
    .SetPaddingX(12)
    .SetPaddingY(8)
    .AddChild(new Label("Stats")
        .SetFontSize(18));
| + +Setters preserve the type: `new Button().SetText("Refresh")` returns a `Button`. Child operations return the **parent**, so the next call continues configuring it. + +## Find an extension + +| Task | Section | +|---|---| +| Build a tree, set a name, or enable an element | [Elements and children](#elements-and-children) | +| Control focus and keyboard navigation | [Focus](#focus) | +| Attach USS and switch classes | [USS and classes](#uss-and-classes) | +| Set dimensions, spacing, colours, and borders | [Styles](#styles) | +| Set a field value and subscribe to changes | [Values and events](#values-and-events) | +| Configure a button, field, or image | [Specific elements](#specific-elements) | +| Create a list that reuses rows | [Lists and trees](#lists-and-trees) | +| Bind a SerializedObject or open a script | [Editor extensions](#editor-extensions) | +| Read a custom USS property as an enum | [Custom USS properties](#custom-uss-properties) | + + + +## Elements and children + +| Before — Unity API | After — FastTools | +|---|---| +|
element.name = name;
|
element.SetName(name);
| +|
element.visible = visible;
|
element.SetVisible(visible);
| +|
element.tooltip = tooltip;
|
element.SetTooltip(tooltip);
| +|
element.userData = data;
|
element.SetUserData(data);
| +|
element.SetEnabled(enabled);
|
element.SetEnabledSelf(enabled);
| +|
element.pickingMode = mode;
|
element.SetPickingMode(mode);
| +|
element.usageHints = hints;
|
element.SetUsageHints(hints);
| +|
element.viewDataKey = key;
|
element.SetViewDataKey(key);
| +|
element.languageDirection = direction;
|
element.SetLanguageDirection(direction);
| +|
element.disablePlayModeTint = disable;
|
element.SetDisablePlayModeTint(disable);
| +|
element.dataSource = source;
|
element.SetDataSource(source);
| +|
element.dataSourceType = type;
|
element.SetDataSourceType(type);
| +|
element.dataSourcePath = path;
|
element.SetDataSourcePath(path);
| + +| Before — Unity API | After — FastTools | +|---|---| +|
panel.Add(child);
|
panel.AddChild(child);
panel.AddChildIf(condition, child);
| +|
foreach (var child in children)
    panel.Add(child);
|
panel.AddChildren(a, b, c);
panel.AddChildren(enumerable);
panel.AddChildren(list);
panel.AddChildren(span);
panel.AddChildren(readOnlySpan);
panel.AddChildrenIf(condition, …);
| +|
panel.Insert(index, child);
|
panel.InsertChild(index, child);
panel.InsertChildIf(condition, index, child);
| +|
foreach (var child in children)
    panel.Insert(index++, child);
|
panel.InsertChildren(index, a, b, c);
panel.InsertChildren(index, enumerable);
panel.InsertChildren(index, list);
panel.InsertChildren(index, span);
panel.InsertChildren(index, readOnlySpan);
panel.InsertChildrenIf(condition, index, …);
| +|
panel.Remove(child);
|
panel.RemoveChild(child);
panel.RemoveChildIf(condition, child);
| +|
panel.RemoveAt(index);
|
panel.RemoveChildAt(index);
panel.RemoveChildAtIf(condition, index);
| +|
panel.Clear();
|
panel.ClearChildren();
panel.ClearChildrenIf(condition);
| + +These methods return the parent element, so they can be chained. `AddChildren` and `InsertChildren` preserve the order of the supplied elements. + +> [!NOTE] +> `*If` checks the condition only at call time. Arguments are evaluated first: `AddChildIf(false, new Label("Warning"))` creates the `Label` but does not add it to the tree. Use a normal `if` when construction is expensive. + +### Visibility and interaction + +| Before — Unity API | After — FastTools | +|---|---| +|
element.visible = false;
|
element.SetVisible(false);
| +|
element.style.display =
    DisplayStyle.None;
|
element.SetDisplay(DisplayStyle.None);
| +|
element.SetEnabled(false);
|
element.SetEnabledSelf(false);
| + + + +## Focus + +| Before — Unity API | After — FastTools | +|---|---| +|
search.Focus();
|
search.FocusSelf();
| +|
search.Blur();
|
search.BlurSelf();
| +|
bool focused =
    search.focusController?.focusedElement
        == search;
|
bool focused = search.IsFocused();
| +|
search.tabIndex = 0;
|
search.SetTabIndex(0);
| +|
search.focusable = true;
|
search.SetFocusable(true);
| +|
search.delegatesFocus = true;
|
search.SetDelegatesFocus(true);
| + + + +## USS and classes + +| Before — Unity API | After — FastTools | +|---|---| +|
panel.AddToClassList("ability-card");
|
panel.AddClass("ability-card");
| +|
panel.RemoveFromClassList("ability-card");
|
panel.RemoveClass("ability-card");
| +|
panel.ClearClassList();
|
panel.ClearClasses();
| +|
panel.ToggleInClassList("playing");
|
panel.ToggleClass("playing");
| +|
panel.EnableInClassList(
    "playing", Application.isPlaying);
|
panel.EnableClass(
    "playing", Application.isPlaying);
| +|
panel.styleSheets.Add(styleSheet);
|
panel.AddStyleSheet(styleSheet);
| +|
panel.styleSheets.Remove(styleSheet);
|
panel.RemoveStyleSheet(styleSheet);
| +|
panel.styleSheets.Add(
    Resources.Load<StyleSheet>("UI/AbilityCard"));
|
panel.AddStyleSheetFromResources("UI/AbilityCard");
| +|
panel.styleSheets.Remove(
    Resources.Load<StyleSheet>("UI/AbilityCard"));
|
panel.RemoveStyleSheetFromResources("UI/AbilityCard");
| + + -Fluent extension methods for building UIToolkit trees in code. All methods return `T` (the element itself) for chaining. +## Styles + +### Sides, axes, and units + +A shared value sets all sides; `X` means left and right, and `Y` means top and bottom. In overloads with optional parameters, omitted sides keep their previous values: ```csharp -using Aspid.FastTools.UIElements; // runtime extensions -using Aspid.FastTools.UIElements.Editors; // editor-only extensions (e.g. AddOpenScriptCommand) +panel + .SetPadding(8) // all sides + .SetPaddingX(12) // left and right + .SetMargin(top: 4, bottom: 8) + .SetSize(width: Length.Percent(100)); ``` -## Example +### Configuring IStyle + +The same methods are available on `element.style`. That chain returns `IStyle`, so resume element methods in a separate call: + +| Before — Unity API | After — FastTools | +|---|---| +|
panel.style.paddingLeft = 12;
panel.style.paddingRight = 12;
panel.style.height = 48;
|
panel.style
    .SetPaddingX(12)
    .SetHeight(48);
| + +### Style reference + +The main examples target Unity 6.0. Disclosure sections mark methods that require newer Unity versions. + +
+Layout + +| Before — Unity API | After — FastTools | +|---|---| +|
panel.style.flexBasis = 120;
|
panel.SetFlexBasis(120);
| +|
panel.style.flexGrow = 1;
|
panel.SetFlexGrow(1);
| +|
panel.style.flexShrink = 0;
|
panel.SetFlexShrink(0);
| +|
panel.style.flexWrap = Wrap.Wrap;
|
panel.SetFlexWrap(Wrap.Wrap);
| +|
panel.style.flexDirection = FlexDirection.Row;
|
panel.SetFlexDirection(FlexDirection.Row);
| +|
panel.style.alignSelf = Align.Center;
|
panel.SetAlignSelf(Align.Center);
| +|
panel.style.alignItems = Align.Center;
|
panel.SetAlignItems(Align.Center);
| +|
panel.style.alignContent = Align.Stretch;
|
panel.SetAlignContent(Align.Stretch);
| +|
panel.style.justifyContent = Justify.SpaceBetween;
|
panel.SetJustifyContent(Justify.SpaceBetween);
| +|
panel.style.position = Position.Absolute;
|
panel.SetPosition(Position.Absolute);
| + +
+ +
+Size + +| Before — Unity API | After — FastTools | +|---|---| +|
panel.style.width = 48;
panel.style.height = 48;
|
panel.SetSize(48);
| +|
panel.style.width = 240;
panel.style.height = 48;
|
panel.SetSize(240, 48);
| +|
panel.style.width = Length.Percent(100);
|
panel.SetSize(width: Length.Percent(100));
| +|
panel.style.minWidth = 120;
panel.style.minHeight = 120;
|
panel.SetMinSize(120);
| +|
panel.style.minWidth = 120;
panel.style.minHeight = 32;
|
panel.SetMinSize(120, 32);
| +|
panel.style.minHeight = 32;
|
panel.SetMinSize(minHeight: 32);
| +|
panel.style.maxWidth = 480;
panel.style.maxHeight = 480;
|
panel.SetMaxSize(480);
| +|
panel.style.maxWidth = 480;
panel.style.maxHeight = 320;
|
panel.SetMaxSize(480, 320);
| +|
panel.style.maxWidth = 480;
|
panel.SetMaxSize(maxWidth: 480);
| +|
panel.style.width = 240;
|
panel.SetWidth(240);
| +|
panel.style.minWidth = 120;
|
panel.SetMinWidth(120);
| +|
panel.style.maxWidth = 480;
|
panel.SetMaxWidth(480);
| +|
panel.style.height = 48;
|
panel.SetHeight(48);
| +|
panel.style.minHeight = 32;
|
panel.SetMinHeight(32);
| +|
panel.style.maxHeight = 320;
|
panel.SetMaxHeight(320);
| + +
+ +
+Spacing and positioning + +| Before — Unity API | After — FastTools | +|---|---| +|
panel.style.marginTop = 8;
panel.style.marginRight = 8;
panel.style.marginBottom = 8;
panel.style.marginLeft = 8;
|
panel.SetMargin(8);
| +|
panel.style.marginTop = 8;
panel.style.marginBottom = 8;
|
panel.SetMargin(top: 8, bottom: 8);
| +|
panel.style.marginLeft = 8;
panel.style.marginRight = 8;
|
panel.SetMarginX(8);
| +|
panel.style.marginTop = 8;
panel.style.marginBottom = 8;
|
panel.SetMarginY(8);
| +|
panel.style.marginTop = 8;
|
panel.SetMarginTop(8);
| +|
panel.style.marginRight = 8;
|
panel.SetMarginRight(8);
| +|
panel.style.marginBottom = 8;
|
panel.SetMarginBottom(8);
| +|
panel.style.marginLeft = 8;
|
panel.SetMarginLeft(8);
| +|
panel.style.paddingTop = 12;
panel.style.paddingRight = 12;
panel.style.paddingBottom = 12;
panel.style.paddingLeft = 12;
|
panel.SetPadding(12);
| +|
panel.style.paddingTop = 12;
panel.style.paddingBottom = 12;
|
panel.SetPadding(top: 12, bottom: 12);
| +|
panel.style.paddingLeft = 12;
panel.style.paddingRight = 12;
|
panel.SetPaddingX(12);
| +|
panel.style.paddingTop = 12;
panel.style.paddingBottom = 12;
|
panel.SetPaddingY(12);
| +|
panel.style.paddingTop = 12;
|
panel.SetPaddingTop(12);
| +|
panel.style.paddingRight = 12;
|
panel.SetPaddingRight(12);
| +|
panel.style.paddingBottom = 12;
|
panel.SetPaddingBottom(12);
| +|
panel.style.paddingLeft = 12;
|
panel.SetPaddingLeft(12);
| +|
panel.style.top = 0;
panel.style.right = 0;
panel.style.bottom = 0;
panel.style.left = 0;
|
panel.SetDistance(0);
| +|
panel.style.top = 0;
panel.style.bottom = 0;
|
panel.SetDistance(top: 0, bottom: 0);
| +|
panel.style.left = 0;
panel.style.right = 0;
|
panel.SetDistanceX(0);
| +|
panel.style.top = 0;
panel.style.bottom = 0;
|
panel.SetDistanceY(0);
| +|
panel.style.top = 0;
|
panel.SetTop(0);
| +|
panel.style.right = 0;
|
panel.SetRight(0);
| +|
panel.style.bottom = 0;
|
panel.SetBottom(0);
| +|
panel.style.left = 0;
|
panel.SetLeft(0);
| + +> `SetDistance` wraps the four `top`/`right`/`bottom`/`left` properties used for absolute positioning. `SetTop`, `SetRight`, `SetBottom`, and `SetLeft` directly alias one property each. + +
+ +
+Font + +| Before — Unity API | After — FastTools | +|---|---| +|
panel.style.unityFont = font;
|
panel.SetUnityFont(font);
| +|
panel.style.fontSize = 14;
|
panel.SetFontSize(14);
| +|
panel.style.unityFontDefinition = fontDefinition;
|
panel.SetUnityFontDefinition(fontDefinition);
| +|
panel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
panel.SetUnityFontStyleAndWeight(FontStyle.Bold);
| + +
+ +
+Font style presets + +Convenience methods toggle bold or italic without overwriting the other flag: + +| Before — Unity API | After — FastTools | +|---|---| +|
panel.style.unityFontStyleAndWeight =
    FontStyle.Normal;
|
panel.SetNormalUnityFontStyleAndWeight();
| +|
var current =
    panel.style.unityFontStyleAndWeight.value;
panel.style.unityFontStyleAndWeight =
    current == FontStyle.Italic
        ? FontStyle.BoldAndItalic
        : FontStyle.Bold;
|
panel.AddBoldUnityFontStyleAndWeight();
| +|
var current =
    panel.style.unityFontStyleAndWeight.value;
panel.style.unityFontStyleAndWeight =
    current == FontStyle.BoldAndItalic
        ? FontStyle.Italic
        : FontStyle.Normal;
|
panel.RemoveBoldUnityFontStyleAndWeight();
| +|
var current =
    panel.style.unityFontStyleAndWeight.value;
panel.style.unityFontStyleAndWeight =
    current == FontStyle.Bold
        ? FontStyle.BoldAndItalic
        : FontStyle.Italic;
|
panel.AddItalicUnityFontStyleAndWeight();
| +|
var current =
    panel.style.unityFontStyleAndWeight.value;
panel.style.unityFontStyleAndWeight =
    current == FontStyle.BoldAndItalic
        ? FontStyle.Bold
        : FontStyle.Normal;
|
panel.RemoveItalicUnityFontStyleAndWeight();
| + +
+ +
+Text + +| Before — Unity API | After — FastTools | +|---|---| +|
panel.style.wordSpacing = 2;
|
panel.SetWordSpacing(2);
| +|
panel.style.letterSpacing = 1;
|
panel.SetLetterSpacing(1);
| +|
panel.style.unityTextAlign = TextAnchor.MiddleCenter;
|
panel.SetUnityTextAlign(TextAnchor.MiddleCenter);
| +|
panel.style.textShadow = shadow;
|
panel.SetTextShadow(shadow);
| +|
panel.style.unityTextOutlineColor = Color.black;
|
panel.SetUnityTextOutlineColor(Color.black);
| +|
panel.style.unityTextOutlineWidth = 1;
|
panel.SetUnityTextOutlineWidth(1);
| +|
panel.style.unityParagraphSpacing = 8;
|
panel.SetUnityParagraphSpacing(8);
| +|
panel.style.textOverflow = TextOverflow.Ellipsis;
|
panel.SetTextOverflow(TextOverflow.Ellipsis);
| +|
panel.style.unityTextOverflowPosition = 
    TextOverflowPosition.End;
|
panel.SetUnityTextOverflowPosition(
    TextOverflowPosition.End);
| +|
panel.style.unityTextGenerator = 
    TextGeneratorType.Advanced;
|
panel.SetUnityTextGenerator(
    TextGeneratorType.Advanced);
| +|
panel.style.unityEditorTextRenderingMode = 
    EditorTextRenderingMode.SDF;
|
panel.SetUnityEditorTextRenderingMode(
    EditorTextRenderingMode.SDF);
| +|
panel.style.whiteSpace = WhiteSpace.NoWrap;
|
panel.SetWhiteSpace(WhiteSpace.NoWrap);
| +|
// Unity 6.2+
panel.style.unityTextAutoSize = autoSize;
|
// Unity 6.2+
panel.SetUnityTextAutoSize(autoSize);
| + +
+ +
+Colour and opacity + +| Before — Unity API | After — FastTools | +|---|---| +|
panel.style.color = Color.white;
|
panel.SetColor(Color.white);
| +|
if (ColorUtility.TryParseHtmlString(
        "#FF8800", out var color))
    panel.style.color = color;
|
panel.SetColor("#FF8800");
| +|
panel.style.opacity = 0.5f;
|
panel.SetOpacity(0.5f);
| + +
+ +
+Border + +| Before — Unity API | After — FastTools | +|---|---| +|
panel.style.borderTopColor = Color.gray;
panel.style.borderRightColor = Color.gray;
panel.style.borderBottomColor = Color.gray;
panel.style.borderLeftColor = Color.gray;
|
panel.SetBorderColor(Color.gray);
| +|
if (ColorUtility.TryParseHtmlString(
        "#333333", out var color))
{
    panel.style.borderTopColor = color;
    panel.style.borderRightColor = color;
    panel.style.borderBottomColor = color;
    panel.style.borderLeftColor = color;
}
|
panel.SetBorderColor("#333333");
| +|
panel.style.borderTopColor = Color.gray;
panel.style.borderBottomColor = Color.gray;
|
panel.SetBorderColor(
    top: Color.gray, bottom: Color.gray);
| +|
panel.style.borderLeftColor = Color.gray;
panel.style.borderRightColor = Color.gray;
|
panel.SetBorderColorX(Color.gray);
| +|
panel.style.borderTopColor = Color.gray;
panel.style.borderBottomColor = Color.gray;
|
panel.SetBorderColorY(Color.gray);
| +|
panel.style.borderTopColor = Color.gray;
|
panel.SetBorderColorTop(Color.gray);
| +|
panel.style.borderRightColor = Color.gray;
|
panel.SetBorderColorRight(Color.gray);
| +|
panel.style.borderBottomColor = Color.gray;
|
panel.SetBorderColorBottom(Color.gray);
| +|
panel.style.borderLeftColor = Color.gray;
|
panel.SetBorderColorLeft(Color.gray);
| +|
panel.style.borderTopLeftRadius = 6;
panel.style.borderTopRightRadius = 6;
panel.style.borderBottomRightRadius = 6;
panel.style.borderBottomLeftRadius = 6;
|
panel.SetBorderRadius(6);
| +|
panel.style.borderTopLeftRadius = 6;
panel.style.borderTopRightRadius = 6;
|
panel.SetBorderRadius(
    topLeft: 6, topRight: 6);
| +|
panel.style.borderTopLeftRadius = 6;
panel.style.borderTopRightRadius = 6;
|
panel.SetBorderRadiusTop(6);
| +|
panel.style.borderBottomLeftRadius = 6;
panel.style.borderBottomRightRadius = 6;
|
panel.SetBorderRadiusBottom(6);
| +|
panel.style.borderTopLeftRadius = 6;
panel.style.borderBottomLeftRadius = 6;
|
panel.SetBorderRadiusLeft(6);
| +|
panel.style.borderTopRightRadius = 6;
panel.style.borderBottomRightRadius = 6;
|
panel.SetBorderRadiusRight(6);
| +|
panel.style.borderTopLeftRadius = 6;
|
panel.SetBorderRadiusTopLeft(6);
| +|
panel.style.borderTopRightRadius = 6;
|
panel.SetBorderRadiusTopRight(6);
| +|
panel.style.borderBottomRightRadius = 6;
|
panel.SetBorderRadiusBottomRight(6);
| +|
panel.style.borderBottomLeftRadius = 6;
|
panel.SetBorderRadiusBottomLeft(6);
| +|
panel.style.borderTopWidth = 1;
panel.style.borderRightWidth = 1;
panel.style.borderBottomWidth = 1;
panel.style.borderLeftWidth = 1;
|
panel.SetBorderWidth(1);
| +|
panel.style.borderTopWidth = 1;
panel.style.borderBottomWidth = 1;
|
panel.SetBorderWidth(top: 1, bottom: 1);
| +|
panel.style.borderLeftWidth = 1;
panel.style.borderRightWidth = 1;
|
panel.SetBorderWidthX(1);
| +|
panel.style.borderTopWidth = 1;
panel.style.borderBottomWidth = 1;
|
panel.SetBorderWidthY(1);
| +|
panel.style.borderTopWidth = 1;
|
panel.SetBorderWidthTop(1);
| +|
panel.style.borderRightWidth = 1;
|
panel.SetBorderWidthRight(1);
| +|
panel.style.borderBottomWidth = 1;
|
panel.SetBorderWidthBottom(1);
| +|
panel.style.borderLeftWidth = 1;
|
panel.SetBorderWidthLeft(1);
| + +
+ +
+Background + +| Before — Unity API | After — FastTools | +|---|---| +|
panel.style.backgroundColor = Color.black;
|
panel.SetBackgroundColor(Color.black);
| +|
if (ColorUtility.TryParseHtmlString(
        "#1B1B1B", out var color))
    panel.style.backgroundColor = color;
|
panel.SetBackgroundColor("#1B1B1B");
| +|
panel.style.backgroundImage = texture;
|
panel.SetBackgroundImage(texture);
| +|
panel.style.backgroundImage =
    Resources.Load<Texture2D>("UI/CardBackground");
|
panel.SetBackgroundImageFromResources(
    "UI/CardBackground");
| +|
panel.style.backgroundSize = backgroundSize;
|
panel.SetBackgroundSize(backgroundSize);
| +|
panel.style.backgroundRepeat = backgroundRepeat;
|
panel.SetBackgroundRepeat(backgroundRepeat);
| +|
panel.style.backgroundPositionX = position;
panel.style.backgroundPositionY = position;
|
panel.SetBackgroundPosition(position);
| +|
panel.style.backgroundPositionY = position;
|
panel.SetBackgroundPosition(y: position);
| +|
panel.style.backgroundPositionX = position;
|
panel.SetBackgroundPositionX(position);
| +|
panel.style.backgroundPositionY = position;
|
panel.SetBackgroundPositionY(position);
| +|
panel.style.unityBackgroundImageTintColor =
    Color.white;
|
panel.SetUnityBackgroundImageTintColor(
    Color.white);
| + +
+ +
+Transform + +| Before — Unity API | After — FastTools | +|---|---| +|
panel.style.scale = new Scale(Vector2.one * 1.2f);
|
panel.SetScale(new Scale(Vector2.one * 1.2f));
| +|
panel.style.rotate = new Rotate(45);
|
panel.SetRotate(new Rotate(45));
| +|
panel.style.translate = new Translate(8, 0);
|
panel.SetTranslate(new Translate(8, 0));
| +|
panel.style.transformOrigin = transformOrigin;
|
panel.SetTransformOrigin(transformOrigin);
| + +
+ +
+Aspect, filter, and material + +Available starting with Unity 6000.3. + +| Before — Unity API | After — FastTools | +|---|---| +|
panel.style.aspectRatio = aspectRatio;
|
panel.SetAspectRatio(aspectRatio);
| +|
panel.style.filter = filter;
|
panel.SetFilter(filter);
| +|
panel.style.unityMaterial = material;
|
panel.SetUnityMaterial(material);
| + +
+ +
+Transition + +| Before — Unity API | After — FastTools | +|---|---| +|
panel.style.transitionDelay =
    new List<TimeValue> { 0.1f };
|
panel.SetTransitionDelay(
    new List<TimeValue> { 0.1f });
| +|
panel.style.transitionDuration =
    new List<TimeValue> { 0.3f };
|
panel.SetTransitionDuration(
    new List<TimeValue> { 0.3f });
| +|
panel.style.transitionProperty =
    new List<StylePropertyName> { "opacity" };
|
panel.SetTransitionProperty(
    new List<StylePropertyName> { "opacity" });
| +|
panel.style.transitionTimingFunction =
    new List<EasingFunction> { EasingMode.EaseInOut };
|
panel.SetTransitionTimingFunction(
    new List<EasingFunction> { EasingMode.EaseInOut });
| + +
+ +
+Overflow and visibility + +| Before — Unity API | After — FastTools | +|---|---| +|
panel.style.overflow = Overflow.Hidden;
|
panel.SetOverflow(Overflow.Hidden);
| +|
panel.style.unityOverflowClipBox = 
    OverflowClipBox.ContentBox;
|
panel.SetUnityOverflowClipBox(
    OverflowClipBox.ContentBox);
| +|
panel.style.visibility = Visibility.Hidden;
|
panel.SetVisibility(Visibility.Hidden);
| +|
panel.style.display = DisplayStyle.None;
|
panel.SetDisplay(DisplayStyle.None);
| + +
+ +
+Image slicing + +| Before — Unity API | After — FastTools | +|---|---| +|
panel.style.unitySliceTop = 4;
panel.style.unitySliceRight = 4;
panel.style.unitySliceBottom = 4;
panel.style.unitySliceLeft = 4;
|
panel.SetUnitySlice(4);
| +|
panel.style.unitySliceTop = 4;
panel.style.unitySliceBottom = 4;
|
panel.SetUnitySlice(top: 4, bottom: 4);
| +|
panel.style.unitySliceLeft = 4;
panel.style.unitySliceRight = 4;
|
panel.SetUnitySliceX(4);
| +|
panel.style.unitySliceTop = 4;
panel.style.unitySliceBottom = 4;
|
panel.SetUnitySliceY(4);
| +|
panel.style.unitySliceTop = 4;
|
panel.SetUnitySliceTop(4);
| +|
panel.style.unitySliceRight = 4;
|
panel.SetUnitySliceRight(4);
| +|
panel.style.unitySliceBottom = 4;
|
panel.SetUnitySliceBottom(4);
| +|
panel.style.unitySliceLeft = 4;
|
panel.SetUnitySliceLeft(4);
| +|
panel.style.unitySliceScale = 1;
|
panel.SetUnitySliceScale(1);
| +|
panel.style.unitySliceType = SliceType.Sliced;
|
panel.SetUnitySliceType(SliceType.Sliced);
| -A reactive editor for an `AbilityConfig` `ScriptableObject` — title and status pill in the header, and a Warning `HelpBox` that toggles based on `ManaCost`. +
-```csharp -[CustomEditor(typeof(AbilityConfig))] -internal sealed class AbilityConfigEditor : Editor -{ - public override VisualElement CreateInspectorGUI() - { - var config = (AbilityConfig)target; - - var badge = new Label() - .SetFontSize(10).SetUnityFontStyleAndWeight(FontStyle.Bold) - .SetPaddingX(10).SetPaddingY(3).SetBorderRadius(10).SetBorderWidth(1); - - var helpBox = new HelpBox("This ability costs no mana — is that intentional?", HelpBoxMessageType.Warning) - .SetMarginTop(8).SetBorderRadius(6); - - Refresh(); - return new VisualElement() - .SetBorderRadius(10).SetBorderWidth(1).SetPaddingX(14).SetPaddingY(12) - .AddChild(new VisualElement() - .SetFlexDirection(FlexDirection.Row).SetAlignItems(Align.Center) - .AddChild(new Label(target.GetScriptName()).SetFlexGrow(1).SetFontSize(15)) - .AddChild(badge)) - .AddChild(new PropertyField(serializedObject.FindProperty("_manaCost")).AddValueChanged(_ => Refresh())) - .AddChild(helpBox); - - void Refresh() - { - var isFree = config.ManaCost is 0; - badge.SetText(isFree ? "FREE" : $"{config.ManaCost} MP"); - helpBox.SetDisplay(isFree ? DisplayStyle.Flex : DisplayStyle.None); - } - } -} -``` +
+Cursor -![The AbilityConfig inspector built with the fluent extensions](Images/aspid_fasttools_visual_element.gif) +| Before — Unity API | After — FastTools | +|---|---| +|
panel.style.cursor = cursor;
|
panel.SetCursor(cursor);
| -## Core element operations +
-```csharp -element - .SetName("MyElement") - .SetVisible(true) - .SetTooltip("Tooltip text") - .AddChild(new Label("Hello")) - .AddChildren(child1, child2, child3); -``` + -| Method | Description | -|--------|-------------| -| `SetName(string)` | Sets `element.name` | -| `SetVisible(bool)` | Sets `element.visible` | -| `SetTooltip(string)` | Sets `element.tooltip` | -| `SetUserData(object)` | Sets `element.userData` | -| `SetEnabledSelf(bool)` | Sets `element.enabledSelf` | -| `SetPickingMode(PickingMode)` | Sets `element.pickingMode` | -| `SetUsageHints(UsageHints)` | Sets `element.usageHints` | -| `SetViewDataKey(string)` | Sets `element.viewDataKey` | -| `SetLanguageDirection(LanguageDirection)` | Sets `element.languageDirection` | -| `SetDisablePlayModeTint(bool)` | Sets `element.disablePlayModeTint` | -| `SetDataSource(object)` | Sets `element.dataSource` | -| `SetDataSourceType(Type)` | Sets `element.dataSourceType` | -| `SetDataSourcePath(PropertyPath)` | Sets `element.dataSourcePath` | -| `AddChild(VisualElement)` | Appends a child, returns the parent | -| `AddChildren(params VisualElement[])` | Appends multiple children | -| `AddChildren(IEnumerable)` | Appends from a sequence | -| `AddChildren(List)` | Appends from a list | -| `AddChildren(Span)` | Appends from a span | -| `AddChildren(ReadOnlySpan)` | Appends from a read-only span | -| `InsertChild(int, VisualElement)` | Inserts a child at the specified index | -| `InsertChildren(int, params VisualElement[])` | Inserts multiple children starting at an index | -| `InsertChildren(int, IEnumerable)` | Inserts from a sequence | -| `InsertChildren(int, List)` | Inserts from a list | -| `InsertChildren(int, Span)` | Inserts from a span | -| `InsertChildren(int, ReadOnlySpan)` | Inserts from a read-only span | -| `RemoveChild(VisualElement)` | Removes a child, returns the parent | -| `RemoveChildAt(int)` | Removes the child at the specified index | -| `ClearChildren()` | Removes all children | - -> Every child operation has an `*If` counterpart (`AddChildIf`, `AddChildrenIf`, `InsertChildIf`, `InsertChildrenIf`, `RemoveChildIf`, `RemoveChildAtIf`, `ClearChildrenIf`) taking a leading `bool condition` — the operation is applied only when the condition is `true`. - -> `RegisterCallbackOnce` and `RegisterCallbackOnce` are available on all Unity versions (polyfill included for versions prior to 2023.1). - -## Focusable - -| Method | Description | -|--------|-------------| -| `FocusSelf()` | Attempts to give focus to the element | -| `BlurSelf()` | Tells the element to release focus | -| `IsFocused()` | Returns whether the element currently has keyboard focus | -| `SetTabIndex(int)` | Sets `element.tabIndex` | -| `SetFocusable(bool)` | Sets `element.focusable` | -| `SetDelegatesFocus(bool)` | Sets `element.delegatesFocus` | - -## USS & class operations - -| Method | Description | -|--------|-------------| -| `AddClass(string)` | Adds a USS class | -| `RemoveClass(string)` | Removes a USS class | -| `ClearClasses()` | Removes all USS classes | -| `ToggleClass(string)` | Toggles a USS class on/off | -| `EnableClass(string, bool)` | Adds or removes a USS class based on a condition | -| `AddStyleSheet(StyleSheet)` | Adds a `StyleSheet` | -| `RemoveStyleSheet(StyleSheet)` | Removes a `StyleSheet` | -| `AddStyleSheetFromResources(string)` | Adds a stylesheet loaded via `Resources.Load` | -| `RemoveStyleSheetFromResources(string)` | Removes a stylesheet loaded via `Resources.Load` | - -## Style extensions — by category - -All style methods are also available on `IStyle` directly (same method names, operate on the style object). - -### Layout - -| Method | Style property | -|--------|---------------| -| `SetFlexBasis(StyleLength)` | `flexBasis` | -| `SetFlexGrow(StyleFloat)` | `flexGrow` | -| `SetFlexShrink(StyleFloat)` | `flexShrink` | -| `SetFlexWrap(StyleEnum)` | `flexWrap` | -| `SetFlexDirection(FlexDirection)` | `flexDirection` | -| `SetAlignSelf(StyleEnum)` | `alignSelf` | -| `SetAlignItems(StyleEnum)` | `alignItems` | -| `SetAlignContent(StyleEnum)` | `alignContent` | -| `SetJustifyContent(StyleEnum)` | `justifyContent` | -| `SetPosition(StyleEnum)` | `position` | - -### Size - -| Method | Description | -|--------|-------------| -| `SetSize(StyleLength)` | Sets both width and height | -| `SetSize(width?, height?)` | Sets width and/or height independently | -| `SetMinSize(StyleLength)` | Sets both minWidth and minHeight | -| `SetMinSize(width?, height?)` | | -| `SetMaxSize(StyleLength)` | Sets both maxWidth and maxHeight | -| `SetMaxSize(width?, height?)` | | -| `SetWidth(StyleLength)` | `width` | -| `SetMinWidth(StyleLength)` | `minWidth` | -| `SetMaxWidth(StyleLength)` | `maxWidth` | -| `SetHeight(StyleLength)` | `height` | -| `SetMinHeight(StyleLength)` | `minHeight` | -| `SetMaxHeight(StyleLength)` | `maxHeight` | - -### Spacing - -All spacing methods have a uniform-value overload, a per-side overload (`top`, `right`, `bottom`, `left`), single-side setters, and X/Y-axis pair setters. - -| Method | Style properties | -|--------|------------------| -| `SetMargin(…)` / `SetPadding(…)` / `SetDistance(…)` | `Top/Right/Bottom/Left` (uniform or per-side) | -| `SetMarginX/Y` · `SetPaddingX/Y` · `SetDistanceX/Y` | Sets the horizontal (X = `Left`+`Right`) or vertical (Y = `Top`+`Bottom`) pair | -| `SetMarginTop/Right/Bottom/Left` | Single-side margin | -| `SetPaddingTop/Right/Bottom/Left` | Single-side padding | -| `SetDistanceTop/Right/Bottom/Left` *(via `SetTop` / `SetRight` / `SetBottom` / `SetLeft`)* | Single-side absolute offset (`top` / `right` / `bottom` / `left` style properties) | - -> `SetDistance` is the wrapper for the four `top`/`right`/`bottom`/`left` style properties used by absolute positioning. `SetTop`, `SetRight`, `SetBottom`, `SetLeft` are direct single-property aliases. - -### Font - -| Method | Style property | -|--------|---------------| -| `SetUnityFont(StyleFont)` | `unityFont` | -| `SetFontSize(StyleLength)` | `fontSize` | -| `SetUnityFontDefinition(StyleFontDefinition)` | `unityFontDefinition` | -| `SetUnityFontStyleAndWeight(StyleEnum)` | `unityFontStyleAndWeight` | - -### Font style presets - -Convenience methods for toggling bold / italic without overwriting the other flag: - -| Method | Description | -|--------|-------------| -| `SetNormalUnityFontStyleAndWeight()` | Resets to `FontStyle.Normal` | -| `AddBoldUnityFontStyleAndWeight()` | Adds bold, preserving italic | -| `RemoveBoldUnityFontStyleAndWeight()` | Removes bold, preserving italic | -| `AddItalicUnityFontStyleAndWeight()` | Adds italic, preserving bold | -| `RemoveItalicUnityFontStyleAndWeight()` | Removes italic, preserving bold | - -### Text - -| Method | Style property | Notes | -|--------|---------------|-------| -| `SetWordSpacing(StyleLength)` | `wordSpacing` | | -| `SetLetterSpacing(StyleLength)` | `letterSpacing` | | -| `SetUnityTextAlign(TextAnchor)` | `unityTextAlign` | | -| `SetTextShadow(StyleTextShadow)` | `textShadow` | | -| `SetUnityTextOutlineColor(StyleColor)` | `unityTextOutlineColor` | | -| `SetUnityTextOutlineWidth(StyleFloat)` | `unityTextOutlineWidth` | | -| `SetUnityParagraphSpacing(StyleLength)` | `unityParagraphSpacing` | | -| `SetTextOverflow(StyleEnum)` | `textOverflow` | | -| `SetUnityTextOverflowPosition(TextOverflowPosition)` | `unityTextOverflowPosition` | | -| `SetUnityTextGenerator(TextGeneratorType)` | `unityTextGenerator` | Unity 6+ | -| `SetUnityEditorTextRenderingMode(EditorTextRenderingMode)` | `unityEditorTextRenderingMode` | Unity 6+ | -| `SetUnityTextAutoSize(StyleTextAutoSize)` | `unityTextAutoSize` | Unity 6.2+ | -| `SetWhiteSpace(StyleEnum)` | `whiteSpace` | | - -### Color & Opacity - -| Method | Style property | -|--------|---------------| -| `SetColor(StyleColor)` | `color` | -| `SetColor(string)` | `color` parsed from an HTML string (`"#RRGGBB"` or a named color) | -| `SetOpacity(StyleFloat)` | `opacity` | - -### Border - -| Method | Description | -|--------|-------------| -| `SetBorderColor(StyleColor)` | All sides | -| `SetBorderColor(top?, right?, bottom?, left?)` | Per side | -| `SetBorderColorX(StyleColor)` · `SetBorderColorY(StyleColor)` | Horizontal (left + right) or vertical (top + bottom) pair | -| `SetBorderColorTop/Right/Bottom/Left(StyleColor)` | Single side | -| `SetBorderRadius(StyleLength)` | All corners | -| `SetBorderRadius(topLeft?, topRight?, bottomLeft?, bottomRight?)` | Per corner | -| `SetBorderRadiusTop(StyleLength)` · `SetBorderRadiusBottom(StyleLength)` | Top or bottom corner pair | -| `SetBorderRadiusTopLeft/TopRight/BottomLeft/BottomRight(StyleLength)` | Single corner | -| `SetBorderWidth(StyleFloat)` | All sides | -| `SetBorderWidth(top?, right?, bottom?, left?)` | Per side | -| `SetBorderWidthX(StyleFloat)` · `SetBorderWidthY(StyleFloat)` | Horizontal or vertical pair | -| `SetBorderWidthTop/Right/Bottom/Left(StyleFloat)` | Single side | - -### Background - -| Method | Style property | -|--------|---------------| -| `SetBackgroundColor(StyleColor)` | `backgroundColor` | -| `SetBackgroundColor(string)` | `backgroundColor` parsed from an HTML string (`"#RRGGBB"` or a named color) | -| `SetBackgroundImage(StyleBackground)` | `backgroundImage` | -| `SetBackgroundImageFromResources(string)` | Loads a `Texture2D` via `Resources.Load` and assigns it to `backgroundImage` | -| `SetBackgroundSize(StyleBackgroundSize)` | `backgroundSize` | -| `SetBackgroundRepeat(StyleBackgroundRepeat)` | `backgroundRepeat` | -| `SetBackgroundPosition(StyleBackgroundPosition)` | Both X and Y | -| `SetBackgroundPosition(x?, y?)` | Independently | -| `SetBackgroundPositionX(StyleBackgroundPosition)` | `backgroundPositionX` | -| `SetBackgroundPositionY(StyleBackgroundPosition)` | `backgroundPositionY` | -| `SetUnityBackgroundImageTintColor(StyleColor)` | `unityBackgroundImageTintColor` | - -### Transform - -| Method | Style property | -|--------|---------------| -| `SetScale(StyleScale)` | `scale` | -| `SetRotate(StyleRotate)` | `rotate` | -| `SetTranslate(StyleTranslate)` | `translate` | -| `SetTransformOrigin(StyleTransformOrigin)` | `transformOrigin` | - -### Aspect, Filter & Material - -Available on Unity 6000.3+. - -| Method | Style property | -|--------|---------------| -| `SetAspectRatio(StyleRatio)` | `aspectRatio` | -| `SetFilter(StyleList)` | `filter` | -| `SetUnityMaterial(StyleMaterialDefinition)` | `unityMaterial` | - -### Transition - -| Method | Style property | -|--------|---------------| -| `SetTransitionDelay(StyleList)` | `transitionDelay` | -| `SetTransitionDuration(StyleList)` | `transitionDuration` | -| `SetTransitionProperty(StyleList)` | `transitionProperty` | -| `SetTransitionTimingFunction(StyleList)` | `transitionTimingFunction` | - -### Overflow & Visibility - -| Method | Style property | -|--------|---------------| -| `SetOverflow(StyleEnum)` | `overflow` | -| `SetUnityOverflowClipBox(StyleEnum)` | `unityOverflowClipBox` | -| `SetVisibility(StyleEnum)` | `visibility` | -| `SetDisplay(DisplayStyle)` | `display` | - -### Unity Slice - -| Method | Description | -|--------|-------------| -| `SetUnitySlice(StyleInt)` | All sides | -| `SetUnitySlice(top?, right?, bottom?, left?)` | Per side | -| `SetUnitySliceX(StyleInt)` · `SetUnitySliceY(StyleInt)` | Horizontal (left + right) or vertical (top + bottom) pair | -| `SetUnitySliceTop/Right/Bottom/Left(StyleInt)` | Single side | -| `SetUnitySliceScale(StyleFloat)` | `unitySliceScale` | -| `SetUnitySliceType(StyleEnum)` | Unity 6+ | - -### Cursor - -| Method | Style property | -|--------|---------------| -| `SetCursor(StyleCursor)` | `cursor` | - -## Specialized element extensions - -### TextElement +## Values and events -```csharp -label - .SetText("Hello World") - .SetEnableRichText(true) - .SetParseEscapeSequences(true); -``` +### Field values -| Method | Description | -|--------|-------------| -| `SetText(string)` | Sets the displayed text | -| `SetEnableRichText(bool)` | Enables rich-text tag parsing | -| `SetEmojiFallbackSupport(bool)` | Enables emoji fallback rendering | -| `SetParseEscapeSequences(bool)` | Whether escape sequences (e.g. `\n`) are parsed | -| `SetDisplayTooltipWhenElided(bool)` | Shows the elided text in a tooltip on hover | +| Before — Unity API | After — FastTools | +|---|---| +|
var field = new IntegerField("Mana cost");
field.value = 42;
field.SetValueWithoutNotify(10);
|
var field = new IntegerField("Mana cost")
    .SetValue(42)
    .SetValue(10, notify: false);
| -### ITextEdition (TextField, IntegerField, …) +### Subscribing and unsubscribing -```csharp -textField - .SetPlaceholder("Search…") - .SetMaxLength(64) - .SetDelayed(true); -``` +| Before — Unity API | After — FastTools | +|---|---| +|
field.RegisterValueChangedCallback(
    onChanged);

field.UnregisterValueChangedCallback(
    onChanged);
|
field.AddValueChanged(onChanged);

field.RemoveValueChanged(onChanged);
| -| Method | Description | -|--------|-------------| -| `SetMaxLength(int)` | Maximum number of characters | -| `SetMaskChar(char)` | Character used to mask password input | -| `SetDelayed(bool)` | Defers value change until focus loss / Enter | -| `SetReadOnly(bool)` | Disables editing | -| `SetPassword(bool)` | Toggles password mode (uses mask char) | -| `SetPlaceholder(string)` | Placeholder text shown when empty | -| `SetAutoCorrection(bool)` | Enables auto-correction (mobile) | -| `SetHideMobileInput(bool)` | Hides the mobile soft input | -| `SetHideSoftKeyboard(bool)` | Hides the on-screen soft keyboard | -| `SetHidePlaceholderOnFocus(bool)` | Removes the placeholder on focus | -| `SetKeyboardType(TouchScreenKeyboardType)` | Sets the touch-screen keyboard type | - -### ITextSelection +
+Value types and Unity.Mathematics integration -```csharp -textField - .SetSelectable(true) - .SetSelectAllOnFocus(true) - .AddOnCursorIndexChange(() => Debug.Log(textField.cursorIndex)); -``` +Typed overloads are available for `int`, `uint`, `nint`, `nuint`, `long`, `ulong`, `short`, `ushort`, `byte`, `sbyte`, `float`, `double`, `decimal`, `char`, `string`, `bool`, `Color`, `Vector2/3/4`, `Vector2Int/3Int`, `Rect/RectInt`, `Bounds/BoundsInt`, `Hash128`, `GUID` (Unity 6.4+), `Quaternion`, `Matrix4x4`, `Gradient`, `AnimationCurve`, `Delegate`, `Enum`, `Object`, and `object`. A generic `SetValue` covers other types. -| Method | Description | -|--------|-------------| -| `AddOnCursorIndexChange(Action)` / `RemoveOnCursorIndexChange(Action)` | Cursor-index change subscription | -| `AddOnSelectIndexChange(Action)` / `RemoveOnSelectIndexChange(Action)` | Selection-index change subscription | -| `SetCursorIndex(int)` | Sets the current cursor index | -| `SetSelectIndex(int)` | Sets the current selection anchor | -| `SetSelectable(bool)` | Whether text can be selected | -| `SetSelectAllOnFocus(bool)` | Selects all text on focus | -| `SetSelectAllOnMouseUp(bool)` | Selects all text on mouse release | -| `SetDoubleClickSelectsWord(bool)` | Double-click selects the word under cursor | -| `SetTripleClickSelectsLine(bool)` | Triple-click selects the line under cursor | +> Installing `com.unity.mathematics` automatically sets `ASPID_FASTTOOLS_UNITY_MATHEMATICS_INTEGRATION` and adds `SetValue` / `AddValueChanged` / `RemoveValueChanged` overloads for `int2/3/4` (and `intMxN`), `float2/3/4` (and `floatMxN`), `half`/`half2/3/4`, `bool2/3/4` (and `boolMxN`), and `quaternion`. -### BaseField\ +
-```csharp -field.SetLabel("My Field"); -field.SetValue(42); -``` +### Buttons and manipulators -### BaseBoolField (Toggle) +An ordinary `VisualElement` can also be clickable. An `out` overload lets you keep the manipulator for removal: ```csharp -toggle - .SetLabel("Enabled") - .SetText("Show advanced settings") - .SetToggleOnLabelClick(true); -``` +panel.AddClickable(Refresh, out var clickable); -| Method | Description | -|--------|-------------| -| `SetText(string)` | Sets the label next to the toggle box | -| `SetLabel(string)` | Sets the field-level label | -| `SetToggleOnLabelClick(bool)` | Whether clicking the label toggles the value | - -### INotifyValueChanged\ - -```csharp -field.SetValue(42, notify: false); // sets value without raising ChangeEvent -field.AddValueChanged(evt => Debug.Log(evt.newValue)); -field.RemoveValueChanged(myCallback); -``` - -Typed overloads are provided for `int`, `uint`, `nint`, `nuint`, `long`, `ulong`, `short`, `ushort`, `byte`, `sbyte`, `float`, `double`, `decimal`, `char`, `string`, `bool`, `Color`, `Vector2/3/4`, `Vector2Int/3Int`, `Rect/RectInt`, `Bounds/BoundsInt`, `Hash128`, `GUID`, `Quaternion`, `Matrix4x4`, `Gradient`, `AnimationCurve`, `Delegate`, `Enum`, `Object`, `object`, plus a generic `SetValue` fallback. - -> When the `com.unity.mathematics` package is installed, the `ASPID_FASTTOOLS_UNITY_MATHEMATICS_INTEGRATION` define is set automatically and adds `SetValue` / `AddValueChanged` / `RemoveValueChanged` overloads for `int2/3/4` (and `intMxN`), `float2/3/4` (and `floatMxN`), `half`/`half2/3/4`, `bool2/3/4` (and `boolMxN`), and `quaternion`. - -### IMixedValueSupport - -```csharp -field.SetShowMixedValue(true); // shows the mixed-value indicator +// When the click is no longer needed +panel.RemoveManipulatorSelf(clickable); ``` -### Button - -```csharp -button - .AddClicked(() => Debug.Log("Clicked")) - .SetClickable(new Clickable(() => { })) - .SetIconImage(myBackground); -``` +| Before — Unity API | After — FastTools | +|---|---| +|
panel.AddManipulator(manipulator);
|
panel.AddManipulatorSelf(manipulator);
| +|
panel.RemoveManipulator(manipulator);
|
panel.RemoveManipulatorSelf(manipulator);
| +|
panel.AddManipulator(new Clickable(Refresh));
|
panel.AddClickable(Refresh);
| +|
var clickable = new Clickable(Refresh);
panel.AddManipulator(clickable);
|
panel.AddClickable(Refresh, out var clickable);
| +|
panel.AddManipulator(
    new Clickable(evt => Refresh()));
|
panel.AddClickable(evt => Refresh());
| +|
panel.AddManipulator(
    new Clickable(Refresh, delay: 500, interval: 100));
|
panel.AddClickable(
    Refresh, delay: 500, interval: 100);
| +|
panel.AddManipulator(
    new KeyboardNavigationManipulator(OnNavigate));
|
panel.AddKeyboardNavigationManipulator(OnNavigate);
| +|
panel.AddManipulator(
    new ContextualMenuManipulator(BuildMenu));
|
panel.AddContextualMenuManipulator(BuildMenu);
| + + + +## Specific elements + +
+TextElement + +| Before — Unity API | After — FastTools | +|---|---| +|
label.text = "Hello World";
|
label.SetText("Hello World");
| +|
label.enableRichText = true;
|
label.SetEnableRichText(true);
| +|
label.emojiFallbackSupport = true;
|
label.SetEmojiFallbackSupport(true);
| +|
label.parseEscapeSequences = true;
|
label.SetParseEscapeSequences(true);
| +|
label.displayTooltipWhenElided = true;
|
label.SetDisplayTooltipWhenElided(true);
| + +
+ +
+ITextEdition (TextField, IntegerField, …) + +| Before — Unity API | After — FastTools | +|---|---| +|
textField.textEdition.maxLength = 64;
|
textField.textEdition.SetMaxLength(64);
| +|
textField.textEdition.maskChar = '*';
|
textField.textEdition.SetMaskChar('*');
| +|
textField.textEdition.isDelayed = true;
|
textField.textEdition.SetDelayed(true);
| +|
textField.textEdition.isReadOnly = true;
|
textField.textEdition.SetReadOnly(true);
| +|
textField.textEdition.isPassword = true;
|
textField.textEdition.SetPassword(true);
| +|
textField.textEdition.placeholder = "Search…";
|
textField.textEdition.SetPlaceholder("Search…");
| +|
textField.textEdition.autoCorrection = true;
|
textField.textEdition.SetAutoCorrection(true);
| +|
textField.textEdition.hideMobileInput = true;
|
textField.textEdition.SetHideMobileInput(true);
| +|
// Unity 6.4+
textField.textEdition.hideSoftKeyboard = true;
|
// Unity 6.4+
textField.textEdition.SetHideSoftKeyboard(true);
| +|
textField.textEdition.hidePlaceholderOnFocus = true;
|
textField.textEdition.SetHidePlaceholderOnFocus(true);
| +|
textField.textEdition.keyboardType =
    TouchScreenKeyboardType.NumberPad;
|
textField.textEdition.SetKeyboardType(
    TouchScreenKeyboardType.NumberPad);
| + +
+ +
+ITextSelection + +| Before — Unity API | After — FastTools | +|---|---| +|
// Unity 6.3+
textField.textSelection.OnCursorIndexChange += OnCursor;
textField.textSelection.OnCursorIndexChange -= OnCursor;
|
// Unity 6.3+
textField.textSelection.AddOnCursorIndexChange(OnCursor);
textField.textSelection.RemoveOnCursorIndexChange(OnCursor);
| +|
// Unity 6.3+
textField.textSelection.OnSelectIndexChange += OnSelect;
textField.textSelection.OnSelectIndexChange -= OnSelect;
|
// Unity 6.3+
textField.textSelection.AddOnSelectIndexChange(OnSelect);
textField.textSelection.RemoveOnSelectIndexChange(OnSelect);
| +|
textField.textSelection.cursorIndex = 0;
|
textField.textSelection.SetCursorIndex(0);
| +|
textField.textSelection.selectIndex = 0;
|
textField.textSelection.SetSelectIndex(0);
| +|
textField.textSelection.isSelectable = true;
|
textField.textSelection.SetSelectable(true);
| +|
textField.textSelection.selectAllOnFocus = true;
|
textField.textSelection.SetSelectAllOnFocus(true);
| +|
textField.textSelection.selectAllOnMouseUp = true;
|
textField.textSelection.SetSelectAllOnMouseUp(true);
| +|
textField.textSelection.doubleClickSelectsWord = true;
|
textField.textSelection.SetDoubleClickSelectsWord(true);
| +|
textField.textSelection.tripleClickSelectsLine = true;
|
textField.textSelection.SetTripleClickSelectsLine(true);
| -| Method | Description | -|--------|-------------| -| `AddClicked(Action)` | Subscribes to `Button.clicked` | -| `RemoveClicked(Action)` | Unsubscribes from `Button.clicked` | -| `SetClickable(Clickable)` | Sets `Button.clickable` | -| `SetIconImage(Background)` | Sets `Button.iconImage` | +
+ +
+BaseField<TValueType> + +| Before — Unity API | After — FastTools | +|---|---| +|
field.label = "Mana cost";
|
field.SetLabel("Mana cost");
| + +
+ +
+BaseBoolField (Toggle) + +| Before — Unity API | After — FastTools | +|---|---| +|
toggle.text = "Show advanced settings";
|
toggle.SetText("Show advanced settings");
| +|
toggle.label = "Enabled";
|
toggle.SetLabel("Enabled");
| +|
toggle.toggleOnLabelClick = true;
|
toggle.SetToggleOnLabelClick(true);
| + +
+ +
+IMixedValueSupport + +| Before — Unity API | After — FastTools | +|---|---| +|
field.showMixedValue = true;
|
field.SetShowMixedValue(true);
| + +
+ +
+Button + +| Before — Unity API | After — FastTools | +|---|---| +|
button.clicked += Refresh;
|
button.AddClicked(Refresh);
| +|
button.clicked -= Refresh;
|
button.RemoveClicked(Refresh);
| +|
button.clickable = clickable;
|
button.SetClickable(clickable);
| +|
button.clickable = new Clickable(Refresh);
|
button.SetClickable(Refresh);
| +|
button.iconImage = iconImage;
|
button.SetIconImage(iconImage);
| + +
+ +
+Slider / BaseSlider<TValue> + +| Before — Unity API | After — FastTools | +|---|---| +|
slider.lowValue = 0f;
|
slider.SetLowValue(0f);
| +|
slider.highValue = 100f;
|
slider.SetHighValue(100f);
| +|
slider.fill = true;
|
slider.SetFill(true);
| +|
slider.inverted = true;
|
slider.SetInverted(true);
| +|
slider.pageSize = 10f;
|
slider.SetPageSize(10f);
| +|
slider.showInputField = true;
|
slider.SetShowInputField(true);
| +|
slider.direction = SliderDirection.Vertical;
|
slider.SetDirection(SliderDirection.Vertical);
| + +
+ +
+ProgressBar + +| Before — Unity API | After — FastTools | +|---|---| +|
progressBar.title = "Loading…";
|
progressBar.SetTitle("Loading…");
| +|
progressBar.lowValue = 0f;
|
progressBar.SetLowValue(0f);
| +|
progressBar.highValue = 100f;
|
progressBar.SetHighValue(100f);
| +|
progressBar.value = 42f;
|
progressBar.SetValue(42f);
| + +
+ +
+HelpBox + +| Before — Unity API | After — FastTools | +|---|---| +|
helpBox.text = "Something went wrong";
|
helpBox.SetText("Something went wrong");
| +|
helpBox.messageType =
    HelpBoxMessageType.Warning;
|
helpBox.SetMessageType(
    HelpBoxMessageType.Warning);
| + +
+ +
+EnumField / EnumFlagsField + +| Before — Unity API | After — FastTools | +|---|---| +|
enumField.Init(
    Mode.Default, includeObsoleteValues: false);
|
enumField.Initialize(
    Mode.Default, includeObsoleteValues: false);
| + +
+ +
+Foldout + +| Before — Unity API | After — FastTools | +|---|---| +|
foldout.text = "Section Title";
|
foldout.SetText("Section Title");
| +|
foldout.toggleOnLabelClick = true;
|
foldout.SetToggleOnLabelClick(true);
| + +
+ +
+Image + +| Before — Unity API | After — FastTools | +|---|---| +|
image.image = texture;
|
image.SetImage(texture);
| +|
image.image =
    Resources.Load<Texture>("UI/Icon");
|
image.SetImageFromResources("UI/Icon");
| +|
image.sprite = sprite;
|
image.SetSprite(sprite);
| +|
image.sprite =
    Resources.Load<Sprite>("UI/Icon");
|
image.SetSpriteFromResources("UI/Icon");
| +|
image.vectorImage = vectorImage;
|
image.SetVectorImage(vectorImage);
| +|
image.vectorImage =
    Resources.Load<VectorImage>("UI/Icon");
|
image.SetVectorImageFromResources("UI/Icon");
| +|
image.uv = new Rect(0, 0, 1, 1);
|
image.SetUv(new Rect(0, 0, 1, 1));
| +|
image.sourceRect = sourceRect;
|
image.SetSourceRect(sourceRect);
| +|
image.tintColor = Color.white;
|
image.SetTintColor(Color.white);
| +|
image.scaleMode = ScaleMode.ScaleToFit;
|
image.SetScaleMode(ScaleMode.ScaleToFit);
| + +
+ +
+IMGUIContainer + +| Before — Unity API | After — FastTools | +|---|---| +|
container.onGUIHandler = OnGUI;
|
container.SetOnGUIHandler(OnGUI);
| +|
container.onGUIHandler += OnGUI;
|
container.AddOnGUIHandler(OnGUI);
| +|
container.onGUIHandler -= OnGUI;
|
container.RemoveOnGUIHandler(OnGUI);
| +|
container.cullingEnabled = true;
|
container.SetCullingEnabled(true);
| +|
container.contextType = ContextType.Editor;
|
container.SetContextType(ContextType.Editor);
| +|
container.MarkDirtyLayout();
|
container.MarkDirtyLayout();
| + +
+ + + +## Lists and trees + +Shared settings apply to `ListView`, `TreeView`, and their `MultiColumn` variants. `SetMakeItem`, `SetBindItem`, `SetUnbindItem`, and `SetDestroyItem` apply to ordinary `ListView` and `TreeView`. + +#### BaseVerticalCollectionView data and behaviour + +| Before — Unity API | After — FastTools | +|---|---| +|
listView.itemsSource = items;
|
listView.SetItemsSource(items);
| +|
listView.reorderable = true;
|
listView.SetReorderable(true);
| +|
listView.selectedIndex = 0;
|
listView.SetSelectedIndex(0);
| +|
listView.selectionType = SelectionType.Single;
|
listView.SetSelectionType(SelectionType.Single);
| +|
listView.fixedItemHeight = 24;
|
listView.SetFixedItemHeight(24);
| +|
listView.virtualizationMethod =
    CollectionVirtualizationMethod.DynamicHeight;
|
listView.SetVirtualizationMethod(
    CollectionVirtualizationMethod.DynamicHeight);
| +|
listView.horizontalScrollingEnabled = true;
|
listView.SetHorizontalScrollingEnabled(true);
| +|
listView.showAlternatingRowBackgrounds =
    AlternatingRowBackground.All;
|
listView.SetShowAlternatingRowBackgrounds(
    AlternatingRowBackground.All);
| + +#### BaseVerticalCollectionView events + +| Before — Unity API | After — FastTools | +|---|---| +|
listView.itemsChosen += OnItemsChosen;
listView.itemsChosen -= OnItemsChosen;
|
listView.AddItemsChosen(OnItemsChosen);
listView.RemoveItemsChosen(OnItemsChosen);
| +|
listView.selectionChanged += OnSelectionChanged;
listView.selectionChanged -= OnSelectionChanged;
|
listView.AddSelectionChanged(OnSelectionChanged);
listView.RemoveSelectionChanged(OnSelectionChanged);
| +|
listView.selectedIndicesChanged += OnIndicesChanged;
listView.selectedIndicesChanged -= OnIndicesChanged;
|
listView.AddSelectedIndicesChanged(OnIndicesChanged);
listView.RemoveSelectedIndicesChanged(OnIndicesChanged);
| +|
listView.itemIndexChanged += OnItemMoved;
listView.itemIndexChanged -= OnItemMoved;
|
listView.AddItemIndexChanged(OnItemMoved);
listView.RemoveItemIndexChanged(OnItemMoved);
| +|
listView.itemsSourceChanged += OnSourceChanged;
listView.itemsSourceChanged -= OnSourceChanged;
|
listView.AddItemsSourceChanged(OnSourceChanged);
listView.RemoveItemsSourceChanged(OnSourceChanged);
| +|
listView.canStartDrag += CanStartDrag;
listView.canStartDrag -= CanStartDrag;
|
listView.AddCanStartDrag(CanStartDrag);
listView.RemoveCanStartDrag(CanStartDrag);
| +|
listView.setupDragAndDrop += SetupDrag;
listView.setupDragAndDrop -= SetupDrag;
|
listView.AddSetupDragAndDrop(SetupDrag);
listView.RemoveSetupDragAndDrop(SetupDrag);
| +|
listView.dragAndDropUpdate += UpdateDrag;
listView.dragAndDropUpdate -= UpdateDrag;
|
listView.AddDragAndDropUpdate(UpdateDrag);
listView.RemoveDragAndDropUpdate(UpdateDrag);
| +|
listView.handleDrop += HandleDrop;
listView.handleDrop -= HandleDrop;
|
listView.AddHandleDrop(HandleDrop);
listView.RemoveHandleDrop(HandleDrop);
| + +#### BaseListView configuration + +| Before — Unity API | After — FastTools | +|---|---| +|
listView.allowAdd = true;
listView.allowRemove = true;
|
listView.SetAllowAdd(true).SetAllowRemove(true);
| +|
listView.headerTitle = "Abilities";
|
listView.SetHeaderTitle("Abilities");
| +|
listView.showFoldoutHeader = true;
|
listView.SetShowFoldoutHeader(true);
| +|
listView.showAddRemoveFooter = true;
|
listView.SetShowAddRemoveFooter(true);
| +|
listView.showBoundCollectionSize = true;
|
listView.SetShowBoundCollectionSize(true);
| +|
listView.reorderMode = ListViewReorderMode.Animated;
|
listView.SetReorderMode(ListViewReorderMode.Animated);
| +|
listView.bindingSourceSelectionMode =
    BindingSourceSelectionMode.AutoAssign;
|
listView.SetBindingSourceSelectionMode(
    BindingSourceSelectionMode.AutoAssign);
| +|
listView.onAdd = OnAdd;
listView.onAdd += OnAdd;
listView.onAdd -= OnAdd;
|
listView.SetOnAdd(OnAdd);
listView.AddOnAdd(OnAdd);
listView.RemoveOnAdd(OnAdd);
| +|
listView.onRemove = OnRemove;
listView.onRemove += OnRemove;
listView.onRemove -= OnRemove;
|
listView.SetOnRemove(OnRemove);
listView.AddOnRemove(OnRemove);
listView.RemoveOnRemove(OnRemove);
| +|
listView.overridingAddButtonBehavior = OnAddButton;
listView.overridingAddButtonBehavior += OnAddButton;
listView.overridingAddButtonBehavior -= OnAddButton;
|
listView.SetOverridingAddButtonBehavior(OnAddButton);
listView.AddOverridingAddButtonBehavior(OnAddButton);
listView.RemoveOverridingAddButtonBehavior(OnAddButton);
| +|
listView.makeFooter = () => new Label();
|
listView.SetMakeFooter(() => new Label());
| +|
listView.makeHeader = () => new Label();
|
listView.SetMakeHeader(() => new Label());
| +|
listView.makeNoneElement =
    () => new Label("No abilities");
|
listView.SetMakeNoneElement(
    () => new Label("No abilities"));
| +|
listView.itemsAdded += OnItemsAdded;
listView.itemsAdded -= OnItemsAdded;
|
listView.AddItemsAdded(OnItemsAdded);
listView.RemoveItemsAdded(OnItemsAdded);
| +|
listView.itemsRemoved += OnItemsRemoved;
listView.itemsRemoved -= OnItemsRemoved;
|
listView.AddItemsRemoved(OnItemsRemoved);
listView.RemoveItemsRemoved(OnItemsRemoved);
| + +#### BaseTreeView configuration + +| Before — Unity API | After — FastTools | +|---|---| +|
treeView.autoExpand = true;
|
treeView.SetAutoExpand(true);
| +|
treeView.itemExpandedChanged += OnExpanded;
treeView.itemExpandedChanged -= OnExpanded;
|
treeView.AddItemExpandedChanged(OnExpanded);
treeView.RemoveItemExpandedChanged(OnExpanded);
| + +#### Creating ListView and TreeView items + +These methods exist in both `ListViewExtensions` and `TreeViewExtensions`, each targeting its own view type. + +| Before — Unity API | After — FastTools | +|---|---| +|
listView.makeItem = () => new Label();
|
listView.SetMakeItem(() => new Label());
| +|
listView.bindItem = BindRow;
listView.bindItem += BindRow;
listView.bindItem -= BindRow;
|
listView.SetBindItem(BindRow);
listView.AddBindItem(BindRow);
listView.RemoveBindItem(BindRow);
| +|
listView.unbindItem = UnbindRow;
listView.unbindItem += UnbindRow;
listView.unbindItem -= UnbindRow;
|
listView.SetUnbindItem(UnbindRow);
listView.AddUnbindItem(UnbindRow);
listView.RemoveUnbindItem(UnbindRow);
| +|
listView.destroyItem = DestroyRow;
listView.destroyItem += DestroyRow;
listView.destroyItem -= DestroyRow;
|
listView.SetDestroyItem(DestroyRow);
listView.AddDestroyItem(DestroyRow);
listView.RemoveDestroyItem(DestroyRow);
| +|
listView.itemTemplate = rowTemplate;
|
listView.SetItemTemplate(rowTemplate);
| -### Slider / BaseSlider\ - -```csharp -slider - .SetLowValue(0f) - .SetHighValue(100f) - .SetShowInputField(true); -``` +#### `MultiColumnListView` / `MultiColumnTreeView` -| Method | Description | -|--------|-------------| -| `SetLowValue(TValue)` | Sets the minimum slider value | -| `SetHighValue(TValue)` | Sets the maximum slider value | -| `SetFill(bool)` | Whether the track is filled up to the current value | -| `SetInverted(bool)` | Reverses the slider direction | -| `SetPageSize(float)` | Controls how much the value changes per page step | -| `SetShowInputField(bool)` | Shows a numeric input field alongside the slider | -| `SetDirection(SliderDirection)` | Sets the slider orientation | +| Before — Unity API | After — FastTools | +|---|---| +|
listView.sortingMode = ColumnSortingMode.Default;
|
listView.SetSortingMode(ColumnSortingMode.Default);
| +|
listView.columnSortingChanged += OnSortingChanged;
listView.columnSortingChanged -= OnSortingChanged;
|
listView.AddColumnSortingChanged(OnSortingChanged);
listView.RemoveColumnSortingChanged(OnSortingChanged);
| -### ProgressBar + -```csharp -progressBar.SetTitle("Loading...").SetLowValue(0f).SetHighValue(100f); -``` +## Editor extensions -| Method | Description | -|--------|-------------| -| `SetTitle(string)` | Sets the title displayed in the center | -| `SetLowValue(float)` | Sets the minimum value | -| `SetHighValue(float)` | Sets the maximum value | +The extensions above work in the editor and at runtime. `SerializedObject` binding and editor commands live in the `Aspid.FastTools.UIElements.Editors` assembly, so this code belongs in an editor assembly such as an `Editor` folder. Add `using Aspid.FastTools.UIElements.Editors;` and `using UnityEditor.UIElements;` to that script. -### HelpBox +### SerializedObject binding -```csharp -helpBox - .SetText("Something went wrong") - .SetMessageType(HelpBoxMessageType.Warning); -``` +| Before — Unity API | After — FastTools | +|---|---| +|
field.bindingPath = "_manaCost";
field.Bind(serializedObject);
|
field.BindTo(
    serializedObject, "_manaCost");
| +|
var property = serializedObject
    .FindProperty("_manaCost");
field.BindProperty(property);
|
var property = serializedObject
    .FindProperty("_manaCost");
field.BindPropertyTo(property);
| +|
root.Bind(serializedObject);
|
root.BindTo(serializedObject);
| +|
root.Unbind();
|
root.UnbindFrom();
| -| Method | Description | -|--------|-------------| -| `SetText(string)` | Sets the help-box message text | -| `SetMessageType(HelpBoxMessageType)` | Sets the icon / severity (`None` / `Info` / `Warning` / `Error`) | +### PropertyField -### Foldout +`PropertyField.AddValueChanged` receives a `SerializedPropertyChangeEvent`. For a regular `IntegerField.AddValueChanged`, the argument is a `ChangeEvent`: ```csharp -foldout - .SetText("Section Title") - .SetToggleOnLabelClick(true) - .SetValue(true); +var manaCost = serializedObject.FindProperty("_manaCost"); +var field = new PropertyField(manaCost) + .SetLabel("Mana cost") + .AddValueChanged(evt => + Debug.Log(evt.changedProperty.intValue)); ``` -| Method | Description | -|--------|-------------| -| `SetText(string)` | Sets the foldout title | -| `SetToggleOnLabelClick(bool)` | Whether clicking the title toggles expansion | - -### Image +To write properties from your own code, see [SerializedProperty Extensions](08-serialized-property-extensions.md). -```csharp -image - .SetImage(myTexture) - .SetTintColor(Color.white) - .SetScaleMode(ScaleMode.ScaleToFit); -``` +### Opening scripts and finding the owner window -| Method | Description | -|--------|-------------| -| `SetImage(Texture)` | Sets `Image.image` | -| `SetImageFromResources(string)` | Loads a texture via `Resources.Load` | -| `SetSprite(Sprite)` | Sets `Image.sprite` | -| `SetSpriteFromResources(string)` | Loads a sprite via `Resources.Load` | -| `SetVectorImage(VectorImage)` | Sets `Image.vectorImage` | -| `SetVectorImageFromResources(string)` | Loads a vector image via `Resources.Load` | -| `SetUv(Rect)` | Sets the UV rect | -| `SetSourceRect(Rect)` | Sets the source rect | -| `SetTintColor(Color)` | Sets the image tint | -| `SetScaleMode(ScaleMode)` | Sets the scale mode | - -### IMGUIContainer +Double-clicking the element opens the script of `target`, a `MonoBehaviour` or `ScriptableObject`, in the IDE. ```csharp -container - .SetOnGUIHandler(() => GUILayout.Label("IMGUI")) - .SetCullingEnabled(true); +image.AddOpenScriptCommand(target); ``` -| Method | Description | -|--------|-------------| -| `SetOnGUIHandler(Action)` | Replaces the `onGUIHandler` callback | -| `AddOnGUIHandler(Action)` | Subscribes to `onGUIHandler` | -| `RemoveOnGUIHandler(Action)` | Unsubscribes from `onGUIHandler` | -| `SetCullingEnabled(bool)` | Skips `onGUIHandler` when the element is offscreen | -| `SetContextType(ContextType)` | Sets the IMGUI context type | -| `MarkDirtyLayout()` | Marks the IMGUI layout dirty so it is recomputed | - -### Collection views (ListView, TreeView, MultiColumn variants) - -Common methods are spread across multiple targeted extensions: - -- `BaseVerticalCollectionViewExtensions` — applies to **all** collection views (ListView, TreeView, MultiColumn variants). -- `BaseListViewExtensions` — applies to ListView and MultiColumnListView. -- `BaseTreeViewExtensions` — applies to TreeView and MultiColumnTreeView. -- `ListViewExtensions` / `TreeViewExtensions` — `MakeItem`/`BindItem`/`UnbindItem`/`DestroyItem` factories per view. -- `MultiColumnListViewExtensions` / `MultiColumnTreeViewExtensions` — multi-column-specific helpers. +`GetOwnerWindow()` looks up the window through the element's panel. If none is found, it falls back to the focused window, then the window under the cursor; the result can be `null`. This helps position a popup when a click has arrived but focus has not switched yet. ```csharp -listView - .SetItemsSource(items) - .SetMakeItem(() => new Label()) - .SetBindItem((el, i) => ((Label)el).SetText(items[i])) - .SetSelectionType(SelectionType.Single) - .AddSelectionChanged(selected => Debug.Log(selected)); +var window = image.GetOwnerWindow(); ``` -#### Source, layout and behavior — `BaseVerticalCollectionView` - -| Method | Description | -|--------|-------------| -| `SetItemsSource(IList)` | Underlying data source | -| `SetReorderable(bool)` | Enables drag-to-reorder | -| `SetSelectedIndex(int)` | Selects a specific index | -| `SetSelectionType(SelectionType)` | None / Single / Multiple | -| `SetFixedItemHeight(float)` | Fixed item height (for `FixedHeight` virtualization) | -| `SetVirtualizationMethod(CollectionVirtualizationMethod)` | `FixedHeight` or `DynamicHeight` | -| `SetHorizontalScrollingEnabled(bool)` | Enables horizontal scrolling | -| `SetShowAlternatingRowBackgrounds(AlternatingRowBackground)` | Zebra striping mode | - -#### Events — `BaseVerticalCollectionView` - -| Method | Description | -|--------|-------------| -| `AddItemsChosen(Action>)` / `RemoveItemsChosen` | Items confirmed (e.g. double-click / Enter) | -| `AddSelectionChanged(Action>)` / `RemoveSelectionChanged` | Selection changed (objects) | -| `AddSelectedIndicesChanged(Action>)` / `RemoveSelectedIndicesChanged` | Selection changed (indices) | -| `AddItemIndexChanged(Action)` / `RemoveItemIndexChanged` | Item moved (drag-reorder) | -| `AddItemsSourceChanged(Action)` / `RemoveItemsSourceChanged` | `itemsSource` reference changed | -| `AddCanStartDrag(Func)` / `RemoveCanStartDrag` | Custom drag-start gating | -| `AddSetupDragAndDrop(Func)` / `RemoveSetupDragAndDrop` | Drag-and-drop preparation | -| `AddDragAndDropUpdate(Func)` / `RemoveDragAndDropUpdate` | Drag-and-drop visual mode | -| `AddHandleDrop(Func)` / `RemoveHandleDrop` | Drop handling | - -#### `BaseListView`-specific - -| Method | Description | -|--------|-------------| -| `SetAllowAdd(bool)` · `SetAllowRemove(bool)` | Toggles built-in add/remove buttons | -| `SetHeaderTitle(string)` | Title shown when foldout header is on | -| `SetShowFoldoutHeader(bool)` | Wraps the list in a `Foldout` | -| `SetShowAddRemoveFooter(bool)` | Toggles the add/remove footer | -| `SetShowBoundCollectionSize(bool)` | Shows the collection-size field | -| `SetReorderMode(ListViewReorderMode)` | `Simple` or `Animated` | -| `SetBindingSourceSelectionMode(BindingSourceSelectionMode)` | Auto-assign / manual | -| `SetOnAdd(Action)` · `AddOnAdd` · `RemoveOnAdd` | Custom add-button callback | -| `SetOnRemove(Action)` · `AddOnRemove` · `RemoveOnRemove` | Custom remove-button callback | -| `SetOverridingAddButtonBehavior(Action)` · `AddOverridingAddButtonBehavior` · `RemoveOverridingAddButtonBehavior` | Replace default add-button click | -| `SetMakeFooter(Func)` · `AddMakeFooter` · `RemoveMakeFooter` | Footer factory (Unity 6+) | -| `SetMakeHeader(Func)` · `AddMakeHeader` · `RemoveMakeHeader` | Header factory (Unity 6+) | -| `SetMakeNoneElement(Func)` · `AddMakeNoneElement` · `RemoveMakeNoneElement` | Empty-state factory (Unity 6+) | -| `AddItemsAdded(Action>)` / `RemoveItemsAdded` | Items added by index | -| `AddItemsRemoved(Action>)` / `RemoveItemsRemoved` | Items removed by index | - -#### `BaseTreeView`-specific - -| Method | Description | -|--------|-------------| -| `SetAutoExpand(bool)` | Auto-expand new nodes | -| `AddItemExpandedChanged(Action)` / `RemoveItemExpandedChanged` | Subscription to expansion changes | - -#### `ListView` / `TreeView` item factories - -These methods are duplicated across `ListViewExtensions` and `TreeViewExtensions` (each operating on its own view type). - -| Method | Description | -|--------|-------------| -| `SetMakeItem(Func)` · `AddMakeItem` · `RemoveMakeItem` | Item factory | -| `SetBindItem(Action)` · `AddBindItem` · `RemoveBindItem` | Item binding | -| `SetUnbindItem(Action)` · `AddUnbindItem` · `RemoveUnbindItem` | Item unbinding | -| `SetDestroyItem(Action)` · `AddDestroyItem` · `RemoveDestroyItem` | Item teardown | -| `SetItemTemplate(VisualTreeAsset)` | UXML template used to build items | - -#### `MultiColumnListView` / `MultiColumnTreeView` - -| Method | Description | -|--------|-------------| -| `SetSortingMode(ColumnSortingMode)` | Built-in sorting mode for the column header | + -## Editor commands (editor-only) +## Custom USS properties -```csharp -using Aspid.FastTools.UIElements.Editors; - -image.AddOpenScriptCommand(target); -// Double-clicking the element opens the script for 'target' in the IDE -``` +Reading a string USS property as an enum inside `CustomStyleResolvedEvent`: -| Method | Target | Description | -|--------|--------|-------------| -| `AddOpenScriptCommand(Object)` | `VisualElement` | Registers a double-click handler that opens the source script for the given `MonoBehaviour` / `ScriptableObject` in the IDE. | -| `GetOwnerWindow()` | `VisualElement` | Returns the `EditorWindow` whose panel hosts the element (falls back to the focused / mouse-over window for detached elements). Use it instead of `EditorWindow.focusedWindow` when anchoring popups to an element — pointer events arrive before focus moves to the clicked window. | -| `BindTo(SerializedObject)` | `VisualElement` | Calls `BindingExtensions.Bind` on the element. | -| `BindTo(SerializedObject, string propertyPath)` | `IBindable` | Sets `bindingPath` and binds to the given `SerializedObject`. | -| `BindPropertyTo(SerializedProperty)` | `IBindable` | Calls `BindingExtensions.BindProperty` with the supplied property. | -| `Initialize(Enum defaultValue, bool includeObsoleteValues = false)` | `EnumField` / `EnumFlagsField` | Initializes the field to the supplied default enum value. | -| `AddValueChanged(EventCallback)` / `RemoveValueChanged(...)` | `PropertyField` | Subscribes / unsubscribes to property change notifications. | +| Before — Unity API | After — FastTools | +|---|---| +|
if (evt.customStyle.TryGetValue(ThemeProperty, out var raw)
    && Enum.TryParse(raw, ignoreCase: true, out PanelTheme theme))
    ApplyTheme(theme);
|
if (evt.customStyle.TryGetByEnum(ThemeProperty, out PanelTheme theme))
    ApplyTheme(theme);
| -## USS custom-style helpers (`ICustomStyle`) +## Practical example -```csharp -using Aspid.FastTools.UIElements; +[EditorTools](../Samples~/EditorTools/Documentation/README.md) contains an ability catalogue and a reactive Inspector built on these extensions: -private static readonly CustomStyleProperty ThemeProperty = new("--aspid-fasttools-prop-theme"); +![Halve cooldown, +5 MP updates both the fields and effect description; Undo restores them.](../Samples~/EditorTools/Documentation/Images/demo.gif) -void OnCustomStyleResolved(CustomStyleResolvedEvent evt) -{ - if (evt.customStyle.TryGetByEnum(ThemeProperty, out ThemeStyle.Type theme)) - ApplyTheme(theme); -} -``` +Halve cooldown, +5 MP updates both the fields and effect description; Undo restores them. -| Method | Description | -|--------|-------------| -| `ICustomStyle.TryGetByEnum(CustomStyleProperty, out T)` | Resolves a string-typed USS custom property and parses it case-insensitively as the enum `T`. Used by every `*Style` struct that exposes a USS-driven enum (`ThemeStyle`, `StatusStyle`, `AspidLabelSizeStyle`, etc.). | diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/08-serialized-property-extensions.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/08-serialized-property-extensions.md index 7354c7eb..a1769696 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/08-serialized-property-extensions.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/08-serialized-property-extensions.md @@ -1,124 +1,202 @@ # SerializedProperty Extensions -Chainable extension methods on `SerializedProperty` for synchronizing the owning `SerializedObject`, setting values, and reflecting on the underlying field. +Chainable extension methods that let a `SerializedProperty` write and apply its own value in one call, without going through its `SerializedObject`. A second group of methods tells which C# field and which object stand behind a property. + +## Quick start + +The examples on this page work with the `AbilityBook` component: ```csharp -using Aspid.FastTools.Editors; +using System; +using System.Collections.Generic; +using UnityEngine; + +public interface IAbilityEffect { } + +public enum Targeting { Single, Area } + +[Flags] +public enum DamageTypes { Fire = 1, Ice = 2, Poison = 4 } + +[Serializable] +public class BurnEffect : IAbilityEffect +{ + public float Damage = 5f; +} + +[Serializable] +public class Ability +{ + public string Name = "Fireball"; +} + +public class AbilityBook : MonoBehaviour +{ + [SerializeField] private int _manaCost = 10; + [SerializeField] private float _cooldown = 1f; + [SerializeField] private Sprite _icon; + [SerializeField] private Targeting _targeting; + [SerializeField] private DamageTypes _damageTypes; + [SerializeField] private List _abilities = new() { new Ability() }; + [SerializeReference] private IAbilityEffect _effect = new BurnEffect(); +} ``` -All extensions are generic over `T : SerializedProperty` and return the same property instance, so calls can be chained freely. +In its custom `Editor`, add `using Aspid.FastTools.Editors;` and write a field value: -## Update / Apply +| Before — Unity API | After — FastTools | +|---|---| +|
var manaCost = serializedObject
    .FindProperty("_manaCost");

serializedObject.Update();
manaCost.intValue = 42;
serializedObject
    .ApplyModifiedProperties();
|
var manaCost = serializedObject
    .FindProperty("_manaCost");

manaCost
    .Update()
    .SetIntAndApply(42);
| + +Setters, `Update()` and both `Apply…()` methods return the original property, so calls chain. Every setter has `AndApply` (with Undo) and `AndApplyWithoutUndo` variants. -Thin wrappers around the matching `SerializedObject` methods on `property.serializedObject`. +**Multiple fields:** update once, write the values, then apply them together: ```csharp -property - .Update() - .SetInt(42) - .ApplyModifiedProperties(); +serializedObject.Update(); +serializedObject.FindProperty("_cooldown").SetFloat(0.5f); +serializedObject.FindProperty("_manaCost").SetIntAndApply(10); ``` -| Method | Description | -|--------|-------------| -| `Update()` | Calls `serializedObject.Update()` | -| `UpdateIfRequiredOrScript()` | Calls `serializedObject.UpdateIfRequiredOrScript()` | -| `ApplyModifiedProperties()` | Calls `serializedObject.ApplyModifiedProperties()` | +> [!IMPORTANT] +> Applying affects **all pending changes** on the associated `SerializedObject`. +> `Update()` discards unapplied writes — call it before changing fields. -## SetValue / SetXxx — typed setters +## Update / Apply -For each supported type four variants exist: +The same Unity operations, called on a property: -| Variant | Behavior | -|---------|----------| -| `SetValue(value)` | Generic dispatch — picks the right typed setter based on the value's runtime type, returns `property` | -| `SetValueAndApply(value)` | `SetValue(value)` followed by `ApplyModifiedProperties()` | -| `SetXxx(value)` | Typed setter (e.g. `SetInt`) that writes to the matching `SerializedProperty.xxxValue` field | -| `SetXxxAndApply(value)` | `SetXxx(value)` followed by `ApplyModifiedProperties()` | +| Before — Unity API | After — FastTools | +|---|---| +|
property.serializedObject
    .Update();
|
property.Update();
| +|
property.serializedObject
    .UpdateIfRequiredOrScript();
|
property
    .UpdateIfRequiredOrScript();
| +|
property.serializedObject
    .ApplyModifiedProperties();
|
property
    .ApplyModifiedProperties();
| +|
property.serializedObject
    .ApplyModifiedPropertiesWithoutUndo();
|
property
    .ApplyModifiedPropertiesWithoutUndo();
| + +## Writing values + +Choose when to apply the write: + +| Before — Unity API | After — FastTools | +|---|---| +|
// Apply later
manaCost.intValue = 42;
|
// Apply later
manaCost.SetInt(42);
| +|
// With Undo
manaCost.intValue = 42;
manaCost.serializedObject
    .ApplyModifiedProperties();
|
// With Undo
manaCost.SetIntAndApply(42);
| +|
// Without Undo
manaCost.intValue = 42;
manaCost.serializedObject
    .ApplyModifiedPropertiesWithoutUndo();
|
// Without Undo
manaCost
    .SetIntAndApplyWithoutUndo(42);
| + +`SetValue` is an alternative to the explicit setter: `SetValue(42)` is equivalent to `SetInt(42)`, and `SetValue(0.5f)` to `SetFloat(0.5f)`. The overload is selected **by the argument’s type**, which must match the field. ### Supported types -| Method family | Unity type | Notes | -|---------------|------------|-------| -| `SetInt` | `int` | | -| `SetUint` | `uint` | | -| `SetLong` | `long` | | -| `SetUlong` | `ulong` | | -| `SetFloat` | `float` | | -| `SetDouble` | `double` | | -| `SetBool` | `bool` | | -| `SetString` | `string` | | -| `SetColor` | `Color` | | -| `SetGradient` | `Gradient` | | -| `SetHash128` | `Hash128` | | -| `SetRect` / `SetRectInt` | `Rect` / `RectInt` | | -| `SetBounds` / `SetBoundsInt` | `Bounds` / `BoundsInt` | | -| `SetVector2` / `SetVector2Int` | `Vector2` / `Vector2Int` | | -| `SetVector3` / `SetVector3Int` | `Vector3` / `Vector3Int` | | -| `SetVector4` | `Vector4` | | -| `SetQuaternion` | `Quaternion` | | -| `SetAnimationCurve` | `AnimationCurve` | | -| `SetEntityId` | `EntityId` (`UnityEngine`) | Unity 6.2+ | - -### Enum setters - -Enum values do not flow through `SetValue` — use the explicit pair below depending on whether the field is a `[Flags]` enum: - -| Method | Description | -|--------|-------------| -| `SetEnumFlag(int)` / `SetEnumFlagAndApply(int)` | Writes to `enumValueFlag` | -| `SetEnumIndex(int)` / `SetEnumIndexAndApply(int)` | Writes to `enumValueIndex` | - -### Example +Every `SerializedProperty` value type has an explicit setter and a `SetValue` overload: + +| Values | Setters | +|---|---| +| Numbers | `SetInt`, `SetUint`, `SetLong`, `SetUlong`, `SetFloat`, `SetDouble` | +| Text, bool and hash | `SetString`, `SetBool`, `SetHash128` | +| Vectors | `SetVector2`, `SetVector2Int`, `SetVector3`, `SetVector3Int`, `SetVector4`, `SetQuaternion` | +| Areas | `SetRect`, `SetRectInt`, `SetBounds`, `SetBoundsInt` | +| Unity types | `SetColor`, `SetGradient`, `SetAnimationCurve` | +| Unity 6.2 and newer | `SetEntityId` for `UnityEngine.EntityId` | + +### Enums + +For the `_targeting` and `_damageTypes` properties: + +| Before — Unity API | After — FastTools | +|---|---| +|
// Targeting.Area
targeting.enumValueIndex = 1;
|
// Targeting.Area
targeting.SetEnumIndex(1);
| +|
// Fire | Ice
damageTypes.enumValueFlag = 3;
|
// Fire | Ice
damageTypes.SetEnumFlag(3);
| + +### Arrays and lists + +For the `_abilities` collection property: + +| Before — Unity API | After — FastTools | +|---|---| +|
abilities.arraySize = 5;
abilities.arraySize += 1;
abilities.arraySize += 2;
abilities.arraySize -= 2;
abilities.arraySize -= 1;
|
abilities.SetArraySize(5);
abilities.AddArraySize();     // +1
abilities.AddArraySize(2);    // +2
abilities.RemoveArraySize(2); // -2
abilities.RemoveArraySize();  // -1
| + +These methods only change the collection size. `RemoveArraySize` removes elements from the end; initialize new elements separately via `GetArrayElementAtIndex()`. + +### References and boxed values + +Choose the setter by how the field is serialized: + +| Before — Unity API | After — FastTools | +|---|---| +|
// [SerializeReference]
effect.managedReferenceValue = instance;
|
// [SerializeReference]
effect.SetManagedReference(instance);
| +|
// UnityEngine.Object
icon.objectReferenceValue = sprite;
|
// UnityEngine.Object
icon.SetObjectReference(sprite);
| +|
// ExposedReference<T>
property.exposedReferenceValue = target;
|
// ExposedReference<T>
property.SetExposedReference(target);
| +|
// boxedValue
property.boxedValue = value;
|
// boxedValue
property.SetBoxed(value);
| + +## Field type and owner + +Three methods find the C# field behind a property through reflection. For the `AbilityBook` from the quick start: ```csharp -SerializedProperty property = GetProperty(); - -// Equivalent forms -property.SetValue(10).ApplyModifiedProperties(); -property.SetValueAndApply(10); -property.SetInt(10).ApplyModifiedProperties(); -property.SetIntAndApply(10); - -// Chain multiple setters -property - .SetVector3(Vector3.up) - .SetBool(true) - .ApplyModifiedProperties(); +var abilities = serializedObject.FindProperty("_abilities"); +var ability = abilities.GetArrayElementAtIndex(0); +var abilityName = ability.FindPropertyRelative("Name"); +var effect = serializedObject.FindProperty("_effect"); +var effectDamage = effect.FindPropertyRelative("Damage"); ``` -## Array operations +| Property | `GetPropertyType()` | `GetFieldInfo()` | `GetDeclaringInstance()` | +|---|---|---|---| +| `abilities` | `List` | `AbilityBook._abilities` | the `AbilityBook` | +| `ability` | `Ability` | `AbilityBook._abilities` | the `AbilityBook` | +| `abilityName` | `string` | `Ability.Name` | the `Ability` at index 0 | +| `effect` | `IAbilityEffect` | `AbilityBook._effect` | the `AbilityBook` | +| `effectDamage` | `float` | `BurnEffect.Damage` | the `BurnEffect` instance | + +- `GetPropertyType()` returns the **declared** field type: for `[SerializeReference]` the interface or base class, not the instance type; for a collection element, the element type. +- `GetFieldInfo()` looks the field up on the owner’s actual type, including private fields of base classes. +- `GetDeclaringInstance()` returns the object that owns the field; for a collection element, the collection’s owner. + +All three methods return `null` when resolution fails: a missing field, a `null` reference on the path or an index outside the list. They read the **first** target object (`targetObject`) and see applied values only, so apply pending writes first. + +> [!WARNING] +> When the field’s owner is a struct, `GetDeclaringInstance()` returns a boxed copy. Changes to that copy never reach the original; write values through the `SerializedProperty` instead. + +## Member name and property checks + +For the same properties: -| Method | Description | -|--------|-------------| -| `SetArraySize(int)` / `SetArraySizeAndApply(int)` | Sets `property.arraySize` | -| `AddArraySize(int = 1)` / `AddArraySizeAndApply(int = 1)` | Increases `arraySize` by the given amount (default `1`) | -| `RemoveArraySize(int = 1)` / `RemoveArraySizeAndApply(int = 1)` | Decreases `arraySize` by the given amount (default `1`) | +| Call | Result | +|---|---| +| `ability.GetMemberName()` | `"_abilities"` — the collection name without the index | +| `abilityName.GetMemberName()` | `"Name"` | +| `ability.IsArrayElement()` | `true` | +| `abilityName.IsArrayElement()` | `false` — a field inside the element | +| `ability.HasFoldout()` | `true` | +| `abilityName.HasFoldout()` | `false` | +| `effect.HasFoldout()` | `false` — `[SerializeReference]` | -## Reference setters +`HasFoldout()` is `true` only for a `Generic` property with visible child properties, as in the default Inspector. `[SerializeReference]` and custom `PropertyDrawer` layouts are not considered. -| Method | Description | Notes | -|--------|-------------|-------| -| `SetManagedReference(object)` / `SetManagedReferenceAndApply(object)` | Writes to `managedReferenceValue` (target must be a `[SerializeReference]` field) | | -| `SetObjectReference(Object)` / `SetObjectReferenceAndApply(Object)` | Writes to `objectReferenceValue` | | -| `SetExposedReference(Object)` / `SetExposedReferenceAndApply(Object)` | Writes to `exposedReferenceValue` | | -| `SetBoxed(object)` / `SetBoxedAndApply(object)` | Writes to `boxedValue` | Unity 6+ | +## Independent property -## Reflection helpers +An inspector’s `SerializedObject` lives only while the inspector is open, so its properties cannot be kept for a deferred call. `Persistent()` returns the same property on a new `SerializedObject` for the same target objects: -For drawer / inspector code that needs to inspect the runtime type or instance behind a property: +| Before — Unity API | After — FastTools | +|---|---| +|
var independentObject =
    new SerializedObject(property
        .serializedObject.targetObjects);
var independent = independentObject
    .FindProperty(property.propertyPath);
|
var independent = property.Persistent();
| -| Method | Returns | Description | -|--------|---------|-------------| -| `GetPropertyType()` | `Type` or `null` | Returns the `FieldType` of the field that backs the property (the element type for an array/list element). `null` if the field can't be resolved. | -| `GetFieldInfo()` | `FieldInfo` or `null` | Locates the backing field by resolving the property's declaring instance (`GetDeclaringInstance`) and looking the field up on its runtime type, base classes included — so a `[SerializeReference]` chain resolves naturally. For an array/list element the collection field is returned (matching `PropertyDrawer.fieldInfo`). | -| `GetDeclaringInstance()` | `object` or `null` | Walks `propertyPath` from the root `targetObject` and returns the runtime instance on which the property's backing field is declared (the owner of the collection field for an array/list element). `null` if the path can't be resolved. A struct owner is returned as a boxed copy — mutations won't reach the serialized object. | +The caller owns the new object: dispose it and the property after the write. ```csharp -public override void OnGUI(Rect rect, SerializedProperty property, GUIContent label) +var independent = manaCost.Persistent(); +if (independent == null) return; + +EditorApplication.delayCall += () => { - var declaringType = property.GetPropertyType(); - var owner = property.GetDeclaringInstance(); - // … -} + using (independent.serializedObject) + using (independent) + independent.Update().SetIntAndApply(42); +}; ``` + +`Persistent()` returns `null` when the property path no longer exists. Pending writes on the source object are not copied; the source view sees the changes after `Update()`. The target objects must stay alive until the deferred call. + +## Package sample + +In [EditorTools](../Samples~/EditorTools/Documentation/README.md), the **Halve cooldown, +5 MP** button writes two properties with `SetFloat` and `SetIntAndApply` as one Undo step. diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/09-editor-helpers.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/09-editor-helpers.md index 24b3125b..e2b78c68 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/09-editor-helpers.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/09-editor-helpers.md @@ -1,27 +1,55 @@ # Editor Helpers -Display-name helpers for Unity objects in custom editors: +`GetDisplayName()` turns an object’s type name into a readable label: `FireAbility` → “Fire Ability”. `GetDisplayNameWithIndex()` adds a number when a GameObject holds several components of that type: “Fire Ability (2)”. -| Method | Returns | -|---|---| -| `GetScriptName()` | The object's display name — `ObjectNames.GetInspectorTitle` when the type has `[AddComponentMenu]`, otherwise the nicified type name | -| `GetScriptNameWithIndex()` | The same name plus a count suffix when the GameObject holds several components of the same type — e.g. `"Audio Source (2)"` | +![Method results in a custom Unity window. These methods do not change the standard Inspector headers.](Images/editor-display-names.png) + +Method results in a custom Unity window. These methods do not change the standard Inspector headers. + +## Quick start + +Two components carry the examples: one names itself through `[AddComponentMenu]`, the other does not. ```csharp -using Aspid.FastTools.Editors; +using UnityEngine; -[CustomEditor(typeof(MyBehaviour))] -public class MyBehaviourEditor : Editor -{ - public override VisualElement CreateInspectorGUI() - { - // "My Behaviour" — or "Custom Name" if [AddComponentMenu("Custom Name")] is present - var name = target.GetScriptName(); +[AddComponentMenu("Gameplay/Fire Ability")] +public sealed class FireAbility : MonoBehaviour { } + +public sealed class AbilityConfig : MonoBehaviour { } +``` + +```csharp +using Aspid.FastTools.Editors; - // "My Behaviour (2)" when a second component of the same type exists - var nameWithIndex = ((Component)target).GetScriptNameWithIndex(); +fireAbility.GetDisplayName(); // "Fire Ability" +abilityConfig.GetDisplayName(); // "Ability Config" - return new Label(name); - } -} +// The second AbilityConfig on the same GameObject +abilityConfig.GetDisplayNameWithIndex(); // "Ability Config (2)" ``` + +> [!NOTE] +> These methods are editor-only. Place calling code in an `Editor` folder or an assembly restricted to the Editor platform. + +## GetDisplayName() + +`GetDisplayName()` extends `UnityEngine.Object`. When the type has an `[AddComponentMenu]` attribute, it uses `ObjectNames.GetInspectorTitle`. Otherwise, it formats the type name with `ObjectNames.NicifyVariableName`. A null or destroyed object returns `string.Empty`. + +| Component | `GetDisplayName()` | `ObjectNames.GetInspectorTitle()` | +|---|---|---| +| `FireAbility`, with the attribute | `Fire Ability` | `Fire Ability` | +| `AbilityConfig`, without it | `Ability Config` | `Ability Config (Script)` | + +## GetDisplayNameWithIndex() + +`GetDisplayNameWithIndex()` extends `Component` and counts components of the **exact same type** on the same GameObject. The suffix follows component order, starting at one. A null or destroyed component returns `string.Empty`. + +| Components on the GameObject | Labels | +|---|---| +| `AbilityConfig` | `Ability Config` | +| `AbilityConfig`, `AbilityConfig` | `Ability Config (1)`, `Ability Config (2)` | + +## Package sample + +In [EditorTools](../Samples~/EditorTools/Documentation/README.md), `GetDisplayName()` supplies the selected ability's pane title. diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/10-claude-code-plugin.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/10-claude-code-plugin.md index 5ce2b302..5708b5c4 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/10-claude-code-plugin.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/10-claude-code-plugin.md @@ -1,21 +1,43 @@ # Claude Code Plugin -If you use [Claude Code](https://docs.claude.com/en/docs/claude-code), the companion [Aspid.Claude.Plugins](https://github.com/VPDPersonal/Aspid.Claude.Plugins) marketplace ships the `aspid-fasttools` plugin — a set of skills that teach Claude Code this package's conventions and APIs. +`aspid-fasttools` adds skills to [Claude Code](https://docs.claude.com/en/docs/claude-code) for profiling methods and building UI with the package’s fluent `VisualElement` extensions. -> [!WARNING] -> The plugin is still in beta — its skills and commands may change between releases. +## Quick start -Add the marketplace and install the plugin: +[Install Aspid.FastTools](README.md#installation) in your Unity project and open the project in Claude Code. Add the marketplace, then install the plugin in the Claude Code session: -```sh +```text /plugin marketplace add VPDPersonal/Aspid.Claude.Plugins ``` -```sh +```text /plugin install aspid-fasttools@aspid-claude-plugins ``` -Included skills: +The plugin is installed separately from the Unity package. Open `/plugin` to check that `aspid-fasttools` is installed. -- **`aspid-profiler-marker`** — insert `this.Marker()` call sites with the right `using`/scope shape. -- **`aspid-visual-element-fluent`** — build editor or runtime UI using the fluent [`VisualElement` extensions](07-visual-element-extensions.md). +## Skills + +Skills activate automatically for matching requests. These two cover features documented in this package: + +| Skill | Task | API guide | +|---|---|---| +| `aspid-profiler-marker` | Add method and block scopes with `this.Marker()` | [ProfilerMarkers](05-profiler-markers.md) | +| `aspid-visual-element-fluent` | Build and style UI Toolkit elements in C# | [VisualElement Extensions](07-visual-element-extensions.md) | + +For example, select a method and ask: + +```text +Add a marker for the entire Simulate method and a separate +named marker for the neighbour search. +Use this.Marker() from Aspid.FastTools. +``` + +Check compilation and inspect the markers in Unity Profiler after applying the changes. + +## Compatibility + +> [!IMPORTANT] +> The plugin is in alpha. Its [documentation](https://github.com/VPDPersonal/Aspid.Claude.Plugins/blob/main/plugins/aspid-fasttools/README.md) targets the earlier `com.aspid.fasttools` package; these guides describe `tech.aspid.fasttools`. Check suggested code against the installed package’s API. + +The plugin also includes `aspid-id-struct` for the earlier package’s `IId` and `[UniqueId]` APIs, which are outside this documentation. The plugin is released independently; see [releases and updates](https://github.com/VPDPersonal/Aspid.Claude.Plugins/releases). diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/11-component-type-selector.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/11-component-type-selector.md new file mode 100644 index 00000000..d29451e3 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/11-component-type-selector.md @@ -0,0 +1,36 @@ +# ComponentTypeSelector + +`ComponentTypeSelector` changes the type of an existing component or ScriptableObject in the Inspector. Switching between subclasses preserves shared field values. + +## Quick start + +Add the field to a base class. The picker offers compatible concrete types and has no `` entry. + +```csharp +using UnityEngine; +using Aspid.FastTools.Types; + +public abstract class EnemyBase : MonoBehaviour +{ + [SerializeField] private ComponentTypeSelector _enemyType; + [SerializeField, Min(0)] private float _health = 100f; +} +``` + +Save the base class as `EnemyBase.cs`. Create subclasses in **separate files** matching their class names: + +| FastEnemy.cs | ArmoredEnemy.cs | +|---|---| +|
using UnityEngine;

public sealed class FastEnemy : EnemyBase
{
    [SerializeField] private float _speed = 25f;
}
|
using UnityEngine;

public sealed class ArmoredEnemy : EnemyBase
{
    [SerializeField] private int _armor = 10;
}
| + +Add **FastEnemy** to a GameObject, set **Health = 75**, and select **ArmoredEnemy** in the picker. The shared `Health` remains, `Speed` disappears, and `Armor` appears. Do not assume fields unique to the previous class will survive a later switch back. + +![Switching a component type with ComponentTypeSelector](Images/component-type-selector.gif) + +Switching a component type with ComponentTypeSelector + +The selected class must have its own script file that Unity recognizes. If no suitable script is found, the type stays unchanged and the Console shows a warning. + +## Package sample + +Try component type switching in the [Types sample](../Samples~/Types/Documentation/README.md). diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/SUMMARY.md.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/11-component-type-selector.md.meta similarity index 75% rename from Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/SUMMARY.md.meta rename to Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/11-component-type-selector.md.meta index cf0c7b64..9d4afdd4 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/SUMMARY.md.meta +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/11-component-type-selector.md.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: beca798c881240019160834a22241bbf +guid: e63664110ca74a54b54c1fb4ed2a2b6d TextScriptImporter: externalObjects: {} userData: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_enum_values.gif b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_enum_values.gif new file mode 100644 index 00000000..6f9c1678 Binary files /dev/null and b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_enum_values.gif differ diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_enum_values.gif.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_enum_values.gif.meta new file mode 100644 index 00000000..ce79b641 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_enum_values.gif.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 20edbc0ddcaa486b89c8e243c80a605a +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_serialize_reference_selector.gif b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_serialize_reference_selector.gif index 48ef295f..45b4e8db 100644 Binary files a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_serialize_reference_selector.gif and b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_serialize_reference_selector.gif differ diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_serialize_reference_tooling.gif b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_serialize_reference_tooling.gif new file mode 100644 index 00000000..3c889a15 Binary files /dev/null and b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_serialize_reference_tooling.gif differ diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_serialize_reference_tooling.gif.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_serialize_reference_tooling.gif.meta new file mode 100644 index 00000000..da1bcf53 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_serialize_reference_tooling.gif.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 5c446266c7754de8b1e4700f5f01f904 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_visual_element.gif b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_visual_element.gif deleted file mode 100644 index e0f00447..00000000 Binary files a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_visual_element.gif and /dev/null differ diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_component_type_selector.gif b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/component-type-selector.gif similarity index 100% rename from Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_component_type_selector.gif rename to Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/component-type-selector.gif diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_component_type_selector.gif.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/component-type-selector.gif.meta similarity index 100% rename from Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_component_type_selector.gif.meta rename to Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/component-type-selector.gif.meta diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/editor-display-names-light.png b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/editor-display-names-light.png new file mode 100644 index 00000000..a7fe763d Binary files /dev/null and b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/editor-display-names-light.png differ diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_visual_element.gif.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/editor-display-names-light.png.meta similarity index 98% rename from Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_visual_element.gif.meta rename to Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/editor-display-names-light.png.meta index 3b65374a..508b31ea 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_visual_element.gif.meta +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/editor-display-names-light.png.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 64b746b29ae0246c5abd41cbad8100d9 +guid: 4fb9201e32094bbabd7b64666a441bda TextureImporter: internalIDToNameTable: [] externalObjects: {} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/editor-display-names.png b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/editor-display-names.png new file mode 100644 index 00000000..c2f21aae Binary files /dev/null and b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/editor-display-names.png differ diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/editor-display-names.png.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/editor-display-names.png.meta new file mode 100644 index 00000000..2c48d881 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/editor-display-names.png.meta @@ -0,0 +1,143 @@ +fileFormatVersion: 2 +guid: f39e12ec819f44719cb0c7dd516574f1 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: iOS + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/enum-values-multipliers-populate.gif b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/enum-values-multipliers-populate.gif new file mode 100644 index 00000000..e9f4930f Binary files /dev/null and b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/enum-values-multipliers-populate.gif differ diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/enum-values-multipliers-populate.gif.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/enum-values-multipliers-populate.gif.meta new file mode 100644 index 00000000..ee6c6530 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/enum-values-multipliers-populate.gif.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 08a5197073d249bea06d2000f3a6a28d +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/enum-values-multipliers-quick-start.png b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/enum-values-multipliers-quick-start.png new file mode 100644 index 00000000..3a477551 Binary files /dev/null and b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/enum-values-multipliers-quick-start.png differ diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/enum-values-multipliers-quick-start.png.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/enum-values-multipliers-quick-start.png.meta new file mode 100644 index 00000000..9f1ef7da --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/enum-values-multipliers-quick-start.png.meta @@ -0,0 +1,143 @@ +fileFormatVersion: 2 +guid: d9c2ab4654504eeb8df350887e8bf1bb +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: iOS + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/enum-values-type-selector.png b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/enum-values-type-selector.png new file mode 100644 index 00000000..9687de57 Binary files /dev/null and b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/enum-values-type-selector.png differ diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/enum-values-type-selector.png.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/enum-values-type-selector.png.meta new file mode 100644 index 00000000..43f85577 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/enum-values-type-selector.png.meta @@ -0,0 +1,143 @@ +fileFormatVersion: 2 +guid: 1ee0d99bdb0b49bb8ef2af66b41376f7 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: iOS + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/profiler-markers-hierarchy-light.svg b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/profiler-markers-hierarchy-light.svg new file mode 100644 index 00000000..433e36f9 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/profiler-markers-hierarchy-light.svg @@ -0,0 +1,44 @@ + + FlockSimulation marker hierarchy + Step (3) contains Steering (5) and Integrate (14). Steering contains Steering.Agent (9), called 120 times. The other markers are called once. All allocate 0 B. Example times: Step 0.38 ms, Steering 0.37 ms, Steering.Agent 0.36 ms, Integrate 0.01 ms. + + + + + + CPU Usage + + Hierarchy + Marker + Calls + GC Alloc + Time ms + + + + FlockSimulation.Step (3) + FlockSimulation.Steering (5) + FlockSimulation.Steering.Agent (9) + FlockSimulation.Integrate (14) + 1 + 1 + 120 + 1 + 0 B + 0.38 + 0 B + 0.37 + 0 B + 0.36 + 0 B + 0.01 + + + + + + + + + + diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/profiler-markers-hierarchy-light.svg.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/profiler-markers-hierarchy-light.svg.meta new file mode 100644 index 00000000..547c9986 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/profiler-markers-hierarchy-light.svg.meta @@ -0,0 +1,53 @@ +fileFormatVersion: 2 +guid: 0e65b4830c8f4f8aa2a198493b1378fe +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12408, guid: 0000000000000000e000000000000000, type: 0} + svgType: 3 + texturedSpriteMeshType: 0 + svgPixelsPerUnit: 100 + gradientResolution: 64 + alignment: 0 + customPivot: {x: 0, y: 0} + generatePhysicsShape: 0 + viewportOptions: 0 + preserveViewport: 0 + advancedMode: 0 + tessellationMode: 1 + predefinedResolutionIndex: 1 + targetResolution: 1080 + resolutionMultiplier: 1 + stepDistance: 10 + samplingStepDistance: 100 + maxCordDeviationEnabled: 0 + maxCordDeviation: 1 + maxTangentAngleEnabled: 0 + maxTangentAngle: 5 + keepTextureAspectRatio: 1 + textureSize: 256 + textureWidth: 256 + textureHeight: 256 + wrapMode: 0 + filterMode: 1 + sampleCount: 4 + preserveSVGImageAspect: 0 + useSVGPixelsPerUnit: 0 + spriteData: + TessellationDetail: 0 + SpriteName: + SpritePivot: {x: 0, y: 0} + SpriteAlignment: 0 + SpriteBorder: {x: 0, y: 0, z: 0, w: 0} + SpriteRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + SpriteID: + PhysicsOutlines: [] diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/profiler-markers-hierarchy.svg b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/profiler-markers-hierarchy.svg new file mode 100644 index 00000000..98b290c5 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/profiler-markers-hierarchy.svg @@ -0,0 +1,44 @@ + + FlockSimulation marker hierarchy + Step (3) contains Steering (5) and Integrate (14). Steering contains Steering.Agent (9), called 120 times. The other markers are called once. All allocate 0 B. Example times: Step 0.38 ms, Steering 0.37 ms, Steering.Agent 0.36 ms, Integrate 0.01 ms. + + + + + + CPU Usage + + Hierarchy + Marker + Calls + GC Alloc + Time ms + + + + FlockSimulation.Step (3) + FlockSimulation.Steering (5) + FlockSimulation.Steering.Agent (9) + FlockSimulation.Integrate (14) + 1 + 1 + 120 + 1 + 0 B + 0.38 + 0 B + 0.37 + 0 B + 0.36 + 0 B + 0.01 + + + + + + + + + + diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/profiler-markers-hierarchy.svg.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/profiler-markers-hierarchy.svg.meta new file mode 100644 index 00000000..46b8f6b3 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/profiler-markers-hierarchy.svg.meta @@ -0,0 +1,53 @@ +fileFormatVersion: 2 +guid: 318335619d0345dcb21b8e3468978985 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12408, guid: 0000000000000000e000000000000000, type: 0} + svgType: 3 + texturedSpriteMeshType: 0 + svgPixelsPerUnit: 100 + gradientResolution: 64 + alignment: 0 + customPivot: {x: 0, y: 0} + generatePhysicsShape: 0 + viewportOptions: 0 + preserveViewport: 0 + advancedMode: 0 + tessellationMode: 1 + predefinedResolutionIndex: 1 + targetResolution: 1080 + resolutionMultiplier: 1 + stepDistance: 10 + samplingStepDistance: 100 + maxCordDeviationEnabled: 0 + maxCordDeviation: 1 + maxTangentAngleEnabled: 0 + maxTangentAngle: 5 + keepTextureAspectRatio: 1 + textureSize: 256 + textureWidth: 256 + textureHeight: 256 + wrapMode: 0 + filterMode: 1 + sampleCount: 4 + preserveSVGImageAspect: 0 + useSVGPixelsPerUnit: 0 + spriteData: + TessellationDetail: 0 + SpriteName: + SpritePivot: {x: 0, y: 0} + SpriteAlignment: 0 + SpriteBorder: {x: 0, y: 0, z: 0, w: 0} + SpriteRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + SpriteID: + PhysicsOutlines: [] diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_serializable_type.gif b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/serializable-type-quick-start.gif similarity index 100% rename from Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_serializable_type.gif rename to Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/serializable-type-quick-start.gif diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_serializable_type.gif.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/serializable-type-quick-start.gif.meta similarity index 100% rename from Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_serializable_type.gif.meta rename to Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/serializable-type-quick-start.gif.meta diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/status-badge-license.svg b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/status-badge-license.svg new file mode 100644 index 00000000..1d03b40d --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/status-badge-license.svg @@ -0,0 +1,18 @@ + + MIT License + + + + + + MIT + diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/status-badge-license.svg.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/status-badge-license.svg.meta new file mode 100644 index 00000000..7d287858 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/status-badge-license.svg.meta @@ -0,0 +1,53 @@ +fileFormatVersion: 2 +guid: d42100f76fe84e2097fc011113aa9604 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12408, guid: 0000000000000000e000000000000000, type: 0} + svgType: 3 + texturedSpriteMeshType: 0 + svgPixelsPerUnit: 100 + gradientResolution: 64 + alignment: 0 + customPivot: {x: 0, y: 0} + generatePhysicsShape: 0 + viewportOptions: 0 + preserveViewport: 0 + advancedMode: 0 + tessellationMode: 1 + predefinedResolutionIndex: 1 + targetResolution: 1080 + resolutionMultiplier: 1 + stepDistance: 10 + samplingStepDistance: 100 + maxCordDeviationEnabled: 0 + maxCordDeviation: 1 + maxTangentAngleEnabled: 0 + maxTangentAngle: 5 + keepTextureAspectRatio: 1 + textureSize: 256 + textureWidth: 256 + textureHeight: 256 + wrapMode: 0 + filterMode: 1 + sampleCount: 4 + preserveSVGImageAspect: 0 + useSVGPixelsPerUnit: 0 + spriteData: + TessellationDetail: 0 + SpriteName: + SpritePivot: {x: 0, y: 0} + SpriteAlignment: 0 + SpriteBorder: {x: 0, y: 0, z: 0, w: 0} + SpriteRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + SpriteID: + PhysicsOutlines: [] diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/status-badge-preview.svg b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/status-badge-preview.svg new file mode 100644 index 00000000..cebf069f --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/status-badge-preview.svg @@ -0,0 +1,19 @@ + + Preview 1.0.0-rc.8 + + + + Preview + 1.0.0-rc.8 + diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/status-badge-preview.svg.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/status-badge-preview.svg.meta new file mode 100644 index 00000000..4d1d8449 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/status-badge-preview.svg.meta @@ -0,0 +1,53 @@ +fileFormatVersion: 2 +guid: 26c182985f774ee6acef0df4ce4aa886 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12408, guid: 0000000000000000e000000000000000, type: 0} + svgType: 3 + texturedSpriteMeshType: 0 + svgPixelsPerUnit: 100 + gradientResolution: 64 + alignment: 0 + customPivot: {x: 0, y: 0} + generatePhysicsShape: 0 + viewportOptions: 0 + preserveViewport: 0 + advancedMode: 0 + tessellationMode: 1 + predefinedResolutionIndex: 1 + targetResolution: 1080 + resolutionMultiplier: 1 + stepDistance: 10 + samplingStepDistance: 100 + maxCordDeviationEnabled: 0 + maxCordDeviation: 1 + maxTangentAngleEnabled: 0 + maxTangentAngle: 5 + keepTextureAspectRatio: 1 + textureSize: 256 + textureWidth: 256 + textureHeight: 256 + wrapMode: 0 + filterMode: 1 + sampleCount: 4 + preserveSVGImageAspect: 0 + useSVGPixelsPerUnit: 0 + spriteData: + TessellationDetail: 0 + SpriteName: + SpritePivot: {x: 0, y: 0} + SpriteAlignment: 0 + SpriteBorder: {x: 0, y: 0, z: 0, w: 0} + SpriteRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + SpriteID: + PhysicsOutlines: [] diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/status-badge-unity.svg b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/status-badge-unity.svg new file mode 100644 index 00000000..a53416d2 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/status-badge-unity.svg @@ -0,0 +1,21 @@ + + Unity 6.0+ + + + + + Unity 6.0+ + diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/status-badge-unity.svg.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/status-badge-unity.svg.meta new file mode 100644 index 00000000..4fe2016f --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/status-badge-unity.svg.meta @@ -0,0 +1,53 @@ +fileFormatVersion: 2 +guid: 918122d78fcf43a78e9e0d0e6cd6b3bf +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12408, guid: 0000000000000000e000000000000000, type: 0} + svgType: 3 + texturedSpriteMeshType: 0 + svgPixelsPerUnit: 100 + gradientResolution: 64 + alignment: 0 + customPivot: {x: 0, y: 0} + generatePhysicsShape: 0 + viewportOptions: 0 + preserveViewport: 0 + advancedMode: 0 + tessellationMode: 1 + predefinedResolutionIndex: 1 + targetResolution: 1080 + resolutionMultiplier: 1 + stepDistance: 10 + samplingStepDistance: 100 + maxCordDeviationEnabled: 0 + maxCordDeviation: 1 + maxTangentAngleEnabled: 0 + maxTangentAngle: 5 + keepTextureAspectRatio: 1 + textureSize: 256 + textureWidth: 256 + textureHeight: 256 + wrapMode: 0 + filterMode: 1 + sampleCount: 4 + preserveSVGImageAspect: 0 + useSVGPixelsPerUnit: 0 + spriteData: + TessellationDetail: 0 + SpriteName: + SpritePivot: {x: 0, y: 0} + SpriteAlignment: 0 + SpriteBorder: {x: 0, y: 0, z: 0, w: 0} + SpriteRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + SpriteID: + PhysicsOutlines: [] diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/type-selector-constraint-warning.png b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/type-selector-constraint-warning.png new file mode 100644 index 00000000..e5faa52b Binary files /dev/null and b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/type-selector-constraint-warning.png differ diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/type-selector-constraint-warning.png.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/type-selector-constraint-warning.png.meta new file mode 100644 index 00000000..b06a0f38 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/type-selector-constraint-warning.png.meta @@ -0,0 +1,143 @@ +fileFormatVersion: 2 +guid: 3ab2f0099c1b49618c6918565783c30c +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: iOS + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_type_selector_display.png b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/type-selector-display.png similarity index 100% rename from Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_type_selector_display.png rename to Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/type-selector-display.png diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_type_selector_display.png.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/type-selector-display.png.meta similarity index 100% rename from Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_type_selector_display.png.meta rename to Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/type-selector-display.png.meta diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_type_selector_generic.gif b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/type-selector-generic.gif similarity index 100% rename from Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_type_selector_generic.gif rename to Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/type-selector-generic.gif diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_type_selector_generic.gif.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/type-selector-generic.gif.meta similarity index 100% rename from Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_type_selector_generic.gif.meta rename to Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/type-selector-generic.gif.meta diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/type-selector-required.png b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/type-selector-required.png new file mode 100644 index 00000000..49ccaaf4 Binary files /dev/null and b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/type-selector-required.png differ diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_type_selector_required.png.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/type-selector-required.png.meta similarity index 100% rename from Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_type_selector_required.png.meta rename to Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/type-selector-required.png.meta diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_type_selector_window.png b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/type-selector-window.png similarity index 100% rename from Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_type_selector_window.png rename to Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/type-selector-window.png diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_type_selector_window.png.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/type-selector-window.png.meta similarity index 100% rename from Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/aspid_fasttools_type_selector_window.png.meta rename to Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/Images/type-selector-window.png.meta diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/README.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/README.md index 2b9e85e3..1c7a0fed 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/README.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/README.md @@ -1,33 +1,131 @@ Aspid.FastTools -# Introduction +[![Unity 6.0+](Images/status-badge-unity.svg)](https://assetstore.unity.com/packages/slug/365584) +[![Preview 1.0.0-rc.8](Images/status-badge-preview.svg)](https://github.com/VPDPersonal/Aspid.FastTools/releases) +[![MIT License](Images/status-badge-license.svg)](https://github.com/VPDPersonal/Aspid.FastTools/blob/main/LICENSE) -**Aspid.FastTools** is a Unity toolset that eliminates routine boilerplate. Inside: a convenient `SerializeReference` workflow (an inspector type picker and a project-wide reference audit window), Roslyn source generators and analyzers, and runtime and editor utilities — from a serializable `System.Type` to fluent UI Toolkit extensions. +Aspid.FastTools is a Unity package that fills the gaps Unity leaves in serialization and editor tooling. Serialized types and polymorphic references stay valid through renames, or get repaired without data loss when they break. The Inspector shows what a `SerializeReference` field holds and lets you swap it. Editor and profiling helpers take a line where Unity needs a class. -[Source Code](https://github.com/VPDPersonal/Aspid.FastTools) · [Unity Asset Store](https://assetstore.unity.com/packages/slug/365584) · [Releases](https://github.com/VPDPersonal/Aspid.FastTools/releases) +[Documentation](https://vpdpersonal.github.io/Aspid.FastTools/docs) · [Source code](https://github.com/VPDPersonal/Aspid.FastTools) · [Releases](https://github.com/VPDPersonal/Aspid.FastTools/releases) -## Getting started +## Installation -[Installation](01-getting-started.md) — UPM git URL, `.unitypackage`, Asset Store, and the samples that ship with the package. +In **Window → Package Manager**, choose **+ → Install package from git URL…** and paste: + +```text +https://github.com/VPDPersonal/Aspid.FastTools.git#upm-preview +``` + +This installs the latest preview, and updating the package brings in a newer one. Git must be installed for UPM Git URLs. + +
+Other installation options + +- **Another version:** copy its UPM tag from [Releases](https://github.com/VPDPersonal/Aspid.FastTools/releases), for example: + + ```text + https://github.com/VPDPersonal/Aspid.FastTools.git#upm-preview/1.0.0-rc.7 + ``` + +- **Unity Asset Store:** the package is not yet available in the store. For now, install it using the Git URL above. +- **`upm` branch:** still holds the older `com.aspid.fasttools` package (`1.0.0-rc.2`). Use the URLs above for `tech.aspid.fasttools` and the features described here. + +
## Features -| Feature | What it gives you | -|---|---| -| [Serializable Type System](02-serializable-types.md) | `System.Type` as a serialized field, `[TypeSelector]`, a searchable type-picker window, `ComponentTypeSelector` | -| [SerializeReference Selector](03-serialize-reference-selector.md) | A type-picker dropdown for `[SerializeReference]` fields, nested inspectors, generics, per-field repair of broken references | -| [SerializeReference Tooling](04-serialize-reference-tooling.md) | Project-wide audit and bulk repair tabs, project settings, the build/CI gate | -| [ProfilerMarkers](05-profiler-markers.md) | Source-generated, per-call-site `ProfilerMarker`s via `this.Marker()` | -| [EnumValues](06-enum-values.md) | Serializable enum → value maps, `[Flags]`-aware, boxing-free | -| [VisualElement Extensions](07-visual-element-extensions.md) | Fluent UI Toolkit tree building in code | -| [SerializedProperty Extensions](08-serialized-property-extensions.md) | Chainable typed setters and reflection helpers | -| [Editor Helpers](09-editor-helpers.md) | Display names for scripts in custom editors | -| [Claude Code Plugin](10-claude-code-plugin.md) | Skills that teach Claude Code this package | +### [Serializable Type System](02-serializable-types.md) + +Store and pick a `System.Type` in the Inspector. + +Select a serializable type in the Inspector + +### [ComponentTypeSelector](11-component-type-selector.md) + +Switch an existing component's type while preserving shared fields. + +Switch a component type in the Inspector + +### [SerializeReference Selector](03-serialize-reference-selector.md) + +Pick which class a `SerializeReference` field holds, straight from the Inspector. + +Switch Pistol to Shotgun while keeping Damage at 37 + +### [SerializeReference Tooling](04-serialize-reference-tooling.md) + +Audit and repair references across the whole project, before builds and in CI. + +Repair a missing weapon type without losing its data + +### [EnumValues](06-enum-values.md) + +Edit enum → value tables in the Inspector, including flags. + +Edit enum keys and their values in the Inspector + +### [ProfilerMarkers](05-profiler-markers.md) + +Generate a unique profiler marker per call site with `this.Marker()`. + +```csharp +using (this.Marker()) +{ + Simulate(); +} +``` + +### [VisualElement Extensions](07-visual-element-extensions.md) + +Build UI Toolkit trees with fluent chains. + +```csharp +new VisualElement() + .SetPadding(8) + .AddChild( + new Label("Stats")); +``` + +### [SerializedProperty Extensions](08-serialized-property-extensions.md) + +Set values, resize arrays, and inspect the field type and owning object. + +```csharp +property + .Update() + .SetIntAndApply(42); +``` + +### [Editor Helpers](09-editor-helpers.md) + +Get readable object and component display names for custom editors. + +```csharp +audio.GetDisplayName(); +// "Audio Source" + +secondAudio + .GetDisplayNameWithIndex(); +// "Audio Source (2)" +``` + +## Quick start + +1. After installation the **Welcome** window opens on its own. Reopen it any time from **Tools → Aspid 🐍 → FastTools → Welcome**. +2. Press **Import** on a sample; it lands in `Assets/Samples`. +3. Open its scene and read the sample's README. + +## Documentation and samples + +- [Samples overview](../Samples~/README.md) — scenes and editor tools for serialization, enum tables, profiling and editor UI. +- [API reference](https://vpdpersonal.github.io/Aspid.FastTools/api/Aspid.FastTools) — public types and members. The feature links above explain how to use them. +- [Claude Code plugin](10-claude-code-plugin.md) — optional skills for working with this package in Claude Code. +- [Changelog](https://github.com/VPDPersonal/Aspid.FastTools/blob/main/CHANGELOG.md) — release history. -## Donate +## Help and support -This project is developed on a voluntary basis. If you find it useful, you can support its development by purchasing the package on the [Unity Asset Store](https://assetstore.unity.com/packages/slug/365584) — that helps allocate more time to improving and maintaining **Aspid.FastTools**. +Report bugs or ask questions in [GitHub Issues](https://github.com/VPDPersonal/Aspid.FastTools/issues). Include your Unity version, package version and steps to reproduce a problem. -## License +Once the package is available on the [Unity Asset Store](https://assetstore.unity.com/packages/slug/365584), you can support development by purchasing it. -**Aspid.FastTools** is distributed under the [MIT License](https://github.com/VPDPersonal/Aspid.FastTools/blob/main/LICENSE). Release history lives in the [CHANGELOG](https://github.com/VPDPersonal/Aspid.FastTools/blob/main/CHANGELOG.md). +Distributed under the [MIT License](https://github.com/VPDPersonal/Aspid.FastTools/blob/main/LICENSE). diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/SUMMARY.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/SUMMARY.md deleted file mode 100644 index 680b6ce9..00000000 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/SUMMARY.md +++ /dev/null @@ -1,20 +0,0 @@ -# Aspid.FastTools Documentation - -The complete guide to Aspid.FastTools for Unity. Rendered at https://vpdpersonal.github.io/Aspid.FastTools/. Russian version: [ru/](ru/README.md). - -## Contents - -1. [Getting Started](01-getting-started.md): installation, samples -2. [Serializable Type System](02-serializable-types.md): `SerializableType`, `[TypeSelector]`, `[TypeSelectorDisplay]`, `TypeSelectorWindow`, `ComponentTypeSelector` -3. [SerializeReference Selector](03-serialize-reference-selector.md): the Inspector dropdown for `[SerializeReference]`, repairing broken references -4. [SerializeReference Tooling](04-serialize-reference-tooling.md): bulk repair tabs, project settings, the build/CI gate -5. [ProfilerMarkers](05-profiler-markers.md): `this.Marker()` and the generated markers -6. [EnumValues](06-enum-values.md): `EnumValues`, `EnumValues` -7. [VisualElement Extensions](07-visual-element-extensions.md): the fluent UI Toolkit API -8. [SerializedProperty Extensions](08-serialized-property-extensions.md): typed setters, arrays, references, reflection helpers -9. [Editor Helpers](09-editor-helpers.md): `GetScriptName`, `GetScriptNameWithIndex` -10. [Claude Code Plugin](10-claude-code-plugin.md): the `aspid-fasttools` plugin - -## Tutorials - -Each sample's `README.md` is its tutorial: [Types](../Samples~/Types/README.md), [SerializeReferences](../Samples~/SerializeReferences/README.md), [EnumValues](../Samples~/EnumValues/README.md), [ProfilerMarkers](../Samples~/ProfilerMarkers/README.md), [EditorTools](../Samples~/EditorTools/README.md). diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/01-getting-started.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/01-getting-started.md deleted file mode 100644 index 0cc778ae..00000000 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/01-getting-started.md +++ /dev/null @@ -1,50 +0,0 @@ -# Начало работы - -## Установка - -Установите Aspid.FastTools через UPM: в Package Manager нажмите **+ → Install package from git URL…** и вставьте один из URL ниже. - -> [!NOTE] -> **Миграция с `com.aspid.fasttools`:** в мае 2026 пакет переименован в `tech.aspid.fasttools`. Для Unity это другой пакет, поэтому установки со старым id не получают обновлений — удалите запись `com.aspid.fasttools` из `Packages/manifest.json` и установите `tech.aspid.fasttools` по одному из URL ниже. - -### Stable - -Ветка `upm` всегда указывает на последний **стабильный** релиз: - -``` -https://github.com/VPDPersonal/Aspid.FastTools.git#upm -``` - -Чтобы установить конкретную версию, укажите неизменяемый per-release тег `upm/` — например, `upm/1.0.0` после выхода релиза 1.0.0 (список доступных версий — на странице [Releases](https://github.com/VPDPersonal/Aspid.FastTools/releases)): - -``` -https://github.com/VPDPersonal/Aspid.FastTools.git#upm/ -``` - -Предпочитаете установку вручную? Скачайте `.unitypackage` со страницы [Releases](https://github.com/VPDPersonal/Aspid.FastTools/releases) или возьмите пакет в [Unity Asset Store](https://assetstore.unity.com/packages/slug/365584). - -### Preview - -Ветка `upm-preview` всегда указывает на последний **preview** релиз (rc, beta, alpha, …): - -``` -https://github.com/VPDPersonal/Aspid.FastTools.git#upm-preview -``` - -Конкретные preview-версии используют ту же схему per-release тегов: - -``` -https://github.com/VPDPersonal/Aspid.FastTools.git#upm-preview/1.0.0-rc.8 -``` - -## Примеры - -К каждой возможности прилагается пример: небольшая сцена или editor-инструмент, который делает с этой возможностью что-то видимое, и `README.md` с тем, что попробовать и куда смотреть в коде. Импортируйте их из Package Manager (**Aspid.FastTools → Samples**) или откройте вкладку **Welcome** (`Tools → Aspid 🐍 → FastTools → Welcome`). - -| Пример | Что показывает | -|---|---| -| [Types](../../Samples~/Types/README.ru.md) | Спавнер врагов: `SerializableMonoScript`, `SerializableType`, `[TypeSelectorDisplay]`, `[TypeSelector]` со ссылкой на член, `ComponentTypeSelector` | -| [SerializeReferences](../../Samples~/SerializeReferences/README.ru.md) | Турель с полиморфным оружием: пикер `[SerializeReference]` во всех формах поля, сломанные ассеты для инструментов ремонта, IMGUI-инспектор | -| [EnumValues](../../Samples~/EnumValues/README.ru.md) | Ходок по плиткам поверхностей: оба варианта `EnumValues`, значения по умолчанию, правила поиска для `[Flags]` | -| [ProfilerMarkers](../../Samples~/ProfilerMarkers/README.ru.md) | Симуляция стаи: сгенерированное дерево маркеров в Profiler | -| [EditorTools](../../Samples~/EditorTools/README.ru.md) | Окно редактора и инспектор: fluent-расширения `VisualElement`, сеттеры `SerializedProperty`, editor-хелперы, `TypeSelectorWindow` | diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/02-serializable-types.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/02-serializable-types.md index 2bf59245..9aaef666 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/02-serializable-types.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/02-serializable-types.md @@ -1,205 +1,199 @@ # Serializable Type System -В Unity нельзя сериализовать `System.Type` из коробки — **Serializable Type System** закрывает -этот пробел: тип выбирается в Инспекторе через иерархическое окно с поиском, хранится как -**assembly-qualified name** и лениво разрешается в `System.Type` при первом обращении. - -**Разделы справочника:** - -* [`SerializableType`](#serializabletype) — сериализуемое поле-обёртка над `System.Type`; -* [`SerializableMonoScript`](#serializablemonoscript) — та же обёртка, но со ссылкой через ассет - скрипта, поэтому переименование класса не ломает поле; -* [`TypeSelectorAttribute`](#typeselectorattribute) — кнопка выбора типа у `string`, - `SerializableType` и `[SerializeReference]` полей, включая - [динамический базовый тип из поля или свойства](#dynamic-base-types-via-member-references); -* [`TypeSelectorDisplay`](#typeselectordisplay) — имя, группа, tooltip и иконка типа-кандидата - в пикере; -* [`TypeSelectorWindow`](#typeselectorwindow) — то же окно выбора как публичный API для - собственного editor-кода; -* [`ComponentTypeSelector`](#componenttypeselector) — выпадающий список в Инспекторе, - меняющий тип компонента или ScriptableObject на подтип. +Выбор типа в инспекторе. Тип сохраняется вместе с компонентом или ассетом и читается в коде как `System.Type`. Обёртки хранят только сам тип — экземпляр создаёт ваш код. Для экземпляра с редактируемыми данными есть [SerializeReference Selector](03-serialize-reference-selector.md). + +## Быстрый старт + +Unity не сериализует поле `System.Type` напрямую. Вместо строки, которую нужно заполнять и разрешать вручную, объявите `SerializableType`. Аргумент `T` ограничивает выбор совместимыми типами. Для примера используем `Collider`: + +| До — строка с именем типа | После — SerializableType | +|---|---| +|
[SerializeField]
private string _colliderTypeName;

public System.Type ColliderType =>
    string.IsNullOrEmpty(_colliderTypeName)
        ? null
        : System.Type.GetType(
            _colliderTypeName, false);
|
[TypeSelector(Allow = TypeAllow.None)]
[SerializeField]
private SerializableType<Collider>
    _colliderType;

public System.Type ColliderType =>
    _colliderType?.Type;
| + +Сама обёртка уже имеет селектор; атрибут здесь исключает абстрактные классы и интерфейсы. + +![Выбор сериализуемого типа в инспекторе](../Images/serializable-type-quick-start.gif) + +Выбор сериализуемого типа в инспекторе + +## Какой инструмент выбрать + +| Задача | Инструмент | +|---|---| +| Хранить тип, включая generic-типы и типы, объявленные внутри другого класса | [`SerializableType`](#serializabletype) | +| Сохранять выбор при переименовании класса и его файла | [`SerializableMonoScript`](#serializablemonoscript) | +| Добавить выбор типа к строке или ограничить поле | [`TypeSelector`](#typeselectorattribute) | +| Хранить экземпляр в `[SerializeReference]` | [SerializeReference Selector](03-serialize-reference-selector.md) | +| Настроить имя, группу, иконку или видимость кандидата | [`TypeSelectorDisplay`](#typeselectordisplay) | +| Открыть окно из редакторского кода | [`TypeSelectorWindow`](#typeselectorwindow) | ## SerializableType -Сериализуемая обёртка над `System.Type`: хранит выбранный тип как assembly-qualified name и лениво разрешает его в `System.Type` при первом обращении. Доступны два варианта: +`SerializableType` хранит assembly-qualified name — имя типа вместе со сборкой. -- **`SerializableType`** — хранит любой тип; -- **`SerializableType`** — хранит тип, ограниченный `T` и его подклассами. +| Вариант | Ограничение выбора | +|---|---| +| `SerializableType` | Без базового ограничения | +| `SerializableType` | Типы, совместимые с `T`, включая реализации интерфейса | -Оба поддерживают неявное преобразование в `System.Type`, создаются из кода конструктором с `Type` -(`new SerializableType(typeof(Dash))` — ограниченная версия бросает исключение для типа, не приводимого к `T`) -и отдают сохранённое имя через `AssemblyQualifiedName`. `SerializableType` наследует `SerializableType`; оба -построены на абстрактной `SerializableTypeBase`. Помните, что Unity сериализует поле по объявленному типу: -`SerializableType`, присвоенный из кода в поле `SerializableType`, загрузится как обёртка без ограничения. +Оба варианта неявно преобразуются в `System.Type` и имеют публичный конструктор с аргументом `Type`: ```csharp -using UnityEngine; -using Aspid.FastTools.Types; +var selected = new SerializableType(typeof(BoxCollider)); +System.Type type = selected; -public abstract class Ability : MonoBehaviour -{ - public abstract void Activate(); -} +var empty = new SerializableType(null); +``` -public sealed class AbilitySelector : MonoBehaviour -{ - [SerializeField] private SerializableType _abilityType; +Тип должен быть совместим с `T`, иначе конструктор выбросит `ArgumentException`. Для пустой обёртки передайте `null`; публичного конструктора без аргументов нет. - private void Start() - { - var ability = (Ability)gameObject.AddComponent(_abilityType.Type); - ability.Activate(); - } -} -``` +| Свойство или вызов | Результат | +|---|---| +| `Type` | Разрешённый `System.Type`; `null`, если выбор пуст или имя больше не разрешается | +| `AssemblyQualifiedName` | Сохранённое имя, даже если тип потерян; пустая строка для пустого выбора | +| `BaseType` | `typeof(object)` либо `typeof(T)` у generic-варианта | +| `ToString()` | Короткое имя найденного типа; иначе сохранённое имя | + +### Пустое значение и переименование + +После переименования класса, namespace или сборки сохранённое имя может перестать разрешаться. Тогда инспектор покажет ``; перед использованием проверяйте `.Type` на `null`. -![Поле SerializableType с выбором типа в Инспекторе](../Images/aspid_fasttools_serializable_type.gif) +> [!NOTE] +> Unity сериализует обёртку по объявленному типу поля. Если присвоить `SerializableType` в поле `SerializableType`, выбранный тип переживёт загрузку, а ограничение `T` — нет. Объявляйте generic-вариант непосредственно у поля. Это же правило относится к `SerializableMonoScript`. ## SerializableMonoScript -То же поле, но ссылка идёт через ассет скрипта, а не через имя типа. `SerializableMonoScript` и -`SerializableMonoScript` хранят рядом с assembly-qualified name редакторскую ссылку на `MonoScript`; в редакторе -источником истины является скрипт, поэтому **переименование или перенос класса не ломает поле** — сохранённое имя -перечитывается из скрипта при каждой сериализации объекта. Ссылка существует только под `UNITY_EDITOR`: сборка плеера -сериализует одно имя, и в рантайме обёртка разрешается по нему точно так же, как `SerializableType`. +`SerializableMonoScript` связывает выбранный тип с ассетом скрипта и сохраняет выбор при согласованном переименовании или переносе класса и файла. Выберите тип в инспекторе или перетащите `.cs` из **Project**. -Ограничение унаследовано от Unity: подходит только тип, которому соответствует ассет скрипта — не вложенный, -не generic класс, объявленный в файле с тем же именем (то, что возвращает `MonoScript.GetClass()`). Пикер показывает -только такие типы, а `MonoScript` можно перетащить на поле из окна Project. Для вложенных и generic-типов нужен -`SerializableType`. +| Хранение имени | Связь с ассетом скрипта | +|---|---| +|
[TypeSelector(Allow = TypeAllow.None)]
[SerializeField]
private SerializableType<MonoBehaviour>
    _componentType;
|
[TypeSelector(Allow = TypeAllow.None)]
[SerializeField]
private SerializableMonoScript<MonoBehaviour>
    _componentType;
| -```csharp -public sealed class EnemySpawner : MonoBehaviour -{ - // Переживёт переименование Grunt в Soldier; пикер предлагает наследников Enemy, у которых есть файл скрипта. - [SerializeField] private SerializableMonoScript _enemyType; - - private void Spawn() => - gameObject.AddComponent(_enemyType.Type); -} -``` +| Возможность | SerializableType | SerializableMonoScript | +|---|---|---| +| Выбор через окно поиска | Да | Да, только типы с подходящим MonoScript | +| Generic-типы и типы, объявленные внутри другого класса | Да | Нет | +| Встроенные типы Unity без ассета `MonoScript`, например `BoxCollider` | Да | Нет | +| Обновление имени после переименования скрипта | Вручную | Из сохранённого MonoScript при сериализации | +| Создание из кода с `Type` | Публичный конструктор | Публичного конструктора нет | +| В плеере | Имя типа | Имя типа; ссылка на MonoScript только в редакторе | -Обёртка, созданная из кода (`new SerializableMonoScript(typeof(Dash))`), хранит только имя типа и становится -устойчивой к переименованию после выбора типа в Инспекторе. +`BoxCollider` поставляется в сборке `UnityEngine.PhysicsModule`, поэтому в проекте нет связанного с ним ассета скрипта. -`[TypeSelector]` (включая `Required = true`) применяется к этим полям так же, как к `SerializableType`. Обе обёртки -`SerializableMonoScript` наследует `SerializableMonoScript`, который делит с `SerializableType` абстрактную -`SerializableTypeBase`, но сам ею не является (сериализованный layout другой). Ассет доступен через редакторское -свойство `Script`. +Скрипт должен содержать класс верхнего уровня, не generic, в файле с соответствующим именем; `MonoScript.GetClass()` должен возвращать этот класс. При переименовании сохраняйте ассет и его `.meta`. Если Unity перестаёт распознавать класс, обёртка оставляет последнее известное имя. + +Читайте выбранный тип через `.Type` или неявное преобразование в `System.Type`, как у `SerializableType`. ## TypeSelectorAttribute -Добавляет к полю в Инспекторе кнопку выбора типа: она открывает иерархическое окно с поиском, в котором перечислены только типы, совместимые с указанными базовыми (при нескольких — со всеми сразу; без аргументов подходит любой тип). Что происходит при выборе, зависит от формы поля: +Атрибут настраивает выбор у поля. Обёртки имеют встроенный селектор и без атрибута; для обычной строки атрибут добавляет его. -- `string` — в поле записывается assembly-qualified имя выбранного типа; -- `SerializableType` / `SerializableType` — сужает встроенный селектор; базовые типы атрибута пересекаются с generic-аргументом `T`; -- managed-ссылка `[SerializeReference]` — выбранный тип сразу инстанцируется в поле (см. [SerializeReference Selector](03-serialize-reference-selector.md)). +| Поле | Результат выбора | +|---|---| +| `string` | Записывается assembly-qualified name | +| `SerializableType` / `SerializableMonoScript` | Настраивается выбор обёртки | +| `[SerializeReference]` | Создаётся экземпляр выбранной реализации | -Атрибут editor-only (`[Conditional("UNITY_EDITOR")]`) и не несёт стоимости в рантайме. +### Ограничения и коллекции ```csharp -using UnityEngine; -using Aspid.FastTools.Types; +[TypeSelector(typeof(MonoBehaviour), Allow = TypeAllow.None)] +[SerializeField] private string _componentTypeName; -public interface IStackable { } - -public abstract class AbilityModifier -{ - public abstract void Apply(); -} - -public sealed class AbilitySelector : MonoBehaviour -{ - // string — сохраняется assembly-qualified имя выбранного типа. - // Каждый элемент массива — отдельный picker, ограниченный AbilityModifier. - [TypeSelector(typeof(AbilityModifier))] - [SerializeField] private string[] _modifierTypes; - - // SerializableType — сужает picker, который у поля уже есть. - [TypeSelector(typeof(AbilityModifier))] - [SerializeField] private SerializableType _modifierType; - - // SerializableType — T сам сужает picker; базовые типы атрибута - // пересекаются с ним: подойдут только реализации AbilityModifier, - // которые заодно являются IStackable. - [TypeSelector(typeof(IStackable))] - [SerializeField] private SerializableType _stackableModifierType; - - // Для [SerializeReference]-поля выбор типа сразу создаёт его экземпляр - // и записывает в поле. Атрибут без аргументов предлагает наследников - // типа поля (здесь — AbilityModifier). Required = true помечает - // незаполненное поле: предупреждение в инспекторе + нарушение CI-гейта. - [TypeSelector(Required = true)] - [SerializeReference] private AbilityModifier _modifier; -} +[TypeSelector(typeof(IDamageable), Allow = TypeAllow.None)] +[SerializeField] private SerializableType _damageableType; + +[TypeSelector(Allow = TypeAllow.None)] +[SerializeField] private SerializableType[] _colliderTypes; ``` +`IDamageable` здесь — ваш интерфейс. В поле `_damageableType` предлагаются компоненты, которые одновременно наследуют `MonoBehaviour` и реализуют `IDamageable`. Все ограничения на строке или обёртке действуют одновременно (**И**). Массивы и списки получают выбор для каждого элемента. + +У `[SerializeReference]` типы в атрибуте задают **альтернативы**. Допустим, `Pistol` и `Rifle` — сериализуемые классы, реализующие `IWeapon`: + +```csharp +[TypeSelector(typeof(Pistol), typeof(Rifle))] +[SerializeReference] private IWeapon _weapon; +``` + +В поле можно выбрать `Pistol` **или** `Rifle`; соответствовать обоим типам одновременно не требуется. Другой класс `Sword : IWeapon` не попадёт в список: одного соответствия типу поля недостаточно. Подробнее — [настройка селектора экземпляров](03-serialize-reference-selector.md#настройка-выбора). + ### Конструкторы и свойства +| Свойство | По умолчанию | Поведение | +|---|---|---| +| `Allow` | `TypeAllow.All` | `Abstract` добавляет абстрактные классы, `Interface` — интерфейсы; `All` включает обе категории, `None` исключает их. На `[SerializeReference]` игнорируется | +| `Required` | `false` | Предупреждает о пустом имени типа или `null` в managed-ссылке | + +Статические классы в списке не отображаются. Для строки или обёртки `Allow` фильтрует категории типов, но не проверяет наличие конструктора без параметров. + +
+Формы аргументов TypeSelector + +```csharp +[TypeSelector] +[TypeSelector(typeof(MonoBehaviour))] +[TypeSelector(typeof(MonoBehaviour), typeof(IDamageable))] +[TypeSelector("Namespace.TypeName, AssemblyName")] +[TypeSelector(nameof(_category))] +``` + +На поле можно поставить один `[TypeSelector]`. Он принимает аргументы `Type` или `string`: один, несколько через запятую (`params`) либо массив. Без аргументов атрибут не добавляет ограничений. Строка сначала ищется как имя поля или свойства; если такой член не найден — как имя типа. + +
+ +### Обязательное поле + ```csharp -[Conditional("UNITY_EDITOR")] -public sealed class TypeSelectorAttribute : PropertyAttribute -{ - public TypeSelectorAttribute() // базовый тип: object - public TypeSelectorAttribute(Type type) - public TypeSelectorAttribute(params Type[] types) - public TypeSelectorAttribute(string assemblyQualifiedName) - public TypeSelectorAttribute(params string[] assemblyQualifiedNames) - - public TypeAllow Allow { get; set; } // по умолчанию: TypeAllow.All - public bool Required { get; set; } // по умолчанию: false -} - -[Flags] -public enum TypeAllow -{ - None = 0, - Abstract = 1, - Interface = 2, - All = Abstract | Interface -} +[TypeSelector(Required = true, Allow = TypeAllow.None)] +[SerializeField] private SerializableType _requiredType; ``` -| Свойство | Описание | -|----------|----------| -| `Allow` | Какие специальные категории типов (абстрактные классы, интерфейсы) включаются в список выбора в дополнение к обычным конкретным классам. По умолчанию: `TypeAllow.All` (поле-имя типа показывает и абстрактные классы, и интерфейсы; укажите `TypeAllow.None`, чтобы ограничить только конкретными типами). Игнорируется на managed-ссылке `[SerializeReference]` | -| `Required` | Помечает незаполненное поле: managed reference `[SerializeReference]`, оставшийся `null`, или пустое `string`-поле показывает предупреждение «required» в инспекторе и считается нарушением для build/CI-гейта. Также покрывает поле `SerializableType` (когда сохранённое имя типа пустое). По умолчанию: `false` | +![Пустое обязательное поле показывает предупреждение рядом с селектором](../Images/type-selector-required.png) -#### Предупреждение Required +Пустое обязательное поле показывает предупреждение рядом с селектором -Так выглядит пустое поле с `Required = true` в Инспекторе: +С `Required = true` пункт `` остаётся доступным: можно очистить поле, и рядом появится предупреждение. У строки или обёртки проверяется пустое сохранённое имя; потерянный тип с непустым именем эту проверку проходит. -![Заполненное поле пикера рядом с пустым Required-полем с inline-предупреждением](../Images/aspid_fasttools_type_selector_required.png) +Настройка проверки по всему проекту и в CI описана в разделе [проверки обязательных полей](04-serialize-reference-tooling.md#где-проверяются-обязательные-поля). -Как находить и чинить такие нарушения по всему проекту из окна FastTools, а не по одному -инспектору за раз, — см. [Bulk repair tabs](04-serialize-reference-tooling.md#bulk-repair-tabs). + -## Dynamic base types via member references +## Ограничение из другого поля -Строковые конструкторы резолвят строку **member-first**: если строка — корректный C#-идентификатор и совпадает с instance-полем или свойством того же объекта, *текущее значение* этого члена задаёт базовый тип(ы) — так одно поле ограничивает пикер другого прямо в Инспекторе, вживую. Любая другая строка трактуется как assembly-qualified имя типа (`Type.GetType`) — то, что нужно для типа, на который в месте вызова нельзя сослаться через `typeof` (за границей editor-сборки или asmdef). +Передайте `nameof(...)`, чтобы текущее значение поля или свойства управляло списком кандидатов. Например, базовая категория и зависящий от неё выбор: ```csharp -public sealed class Loadout : MonoBehaviour -{ - // Выбранная здесь категория управляет пикером _weaponType ниже. - [SerializeField] private SerializableType _category; - - // Ограничен вживую тем, что сейчас лежит в _category. - [TypeSelector(nameof(_category))] - [SerializeField] private string _weaponType; -} +[SerializeField] private SerializableType _category; + +[TypeSelector(nameof(_category), Allow = TypeAllow.None)] +[SerializeField] private string _componentTypeName; ``` -Член должен быть instance-полем или свойством типа `Type`, `string`, `SerializableType` / `SerializableType` — либо массивом любого из них. Предпочитайте `nameof(...)`, чтобы переименование не рвало ссылку. Неизвестное имя или член неподходящего вида — это **ошибка компиляции** (правила анализатора `AFT0006`–`AFT0008`); в случаях, которые анализатор не видит (precompiled-сборки, переименование без перекомпиляции), drawer вместо этого показывает inline-предупреждение под полем. +Измените **Category**, затем откройте **Component Type Name**: список будет ограничен выбранным типом и его наследниками. Смена ограничения сама по себе не очищает ранее выбранное имя — проверьте зависимое поле и при необходимости выберите тип заново. + +| Источник ограничения | Поддержка | +|---|---| +| `System.Type` | Один тип | +| `string` | Имя типа, разрешаемое через `Type.GetType` | +| `SerializableType`, `SerializableMonoScript` и их generic-варианты | Разрешённое значение `.Type` | +| Массив этих значений | Несколько ограничений одновременно | + +Источник — нестатическое поле или читаемое свойство объекта, который редактирует инспектор. Подходят и унаследованные члены; индексаторы не поддерживаются. Пустой источник не добавляет ограничения. У generic-обёртки её собственный `T` продолжает ограничивать выбор. + +Для типа используйте `typeof`, для поля или свойства — `nameof`. Если строка не указывает ни на член объекта, ни на доступный тип, инспектор показывает предупреждение. + +![Опечатка _categroy вместо _category вызывает предупреждение. nameof(_category) помогает избежать такой ошибки.](../Images/type-selector-constraint-warning.png) + +Опечатка _categroy вместо _category вызывает предупреждение. nameof(_category) помогает избежать такой ошибки. ## TypeSelectorDisplay -Пометьте тип-кандидат атрибутом `[TypeSelectorDisplay]`, чтобы настроить, как он показывается в селекторе — это editor-only атрибут (`[Conditional("UNITY_EDITOR")]`) в `Aspid.FastTools.Types`, не несущий стоимости в рантайме. Компилятор проверяет это условие там, где атрибут *написан*, поэтому объявляйте его внутри Unity-проекта: у типа, собранного вне Unity (плагин-`.dll` из `dotnet build`), не действует ни одна из этих настроек, включая `Hidden`. +`TypeSelectorDisplay` задаёт подпись, группу, иконку и подсказку типа в окне выбора: ```csharp using Aspid.FastTools.Types; -// Переименовать тип в пикере, положить его в явную группу, задать tooltip и иконку: [TypeSelectorDisplay( Name = "Damage ×", Group = "Combat/Modifiers", @@ -208,102 +202,107 @@ using Aspid.FastTools.Types; public sealed class DamageModifier { } ``` -| Член | Описание | -|------|----------| -| `Name` | Отображаемое имя вместо короткого имени типа — в строках пикера и в подписи закрытого дропдауна. Поиск по-прежнему находит тип и по настоящему имени, а tooltip при наведении показывает полную идентичность `Namespace.Class, Assembly`. `null` или пробелы — без переопределения. | -| `Group` | Явный путь в пикере, уровни разделяются `/` (например `"Combat/Melee"`). **Заменяет** размещение по namespace — тип показывается только под этим путём, сегменты пути общие для разных типов. `null` или пробелы — размещение по namespace. | -| `Tooltip` | Tooltip, показываемый при наведении на строку типа. `null` — без переопределения tooltip. | -| `Icon` | Иконка редактора слева от лейбла — имя `EditorGUIUtility.IconContent`, путь к ассету в проекте с расширением (загружается через `AssetDatabase`) или путь к текстуре в `Resources` без расширения. `null` — без иконки. | -| `Hidden` | При `true` пикер никогда не предлагает тип. Не наследуется, поэтому наследники, которые должны прийти скрытому типу на замену, остаются доступными как обычно. Присваивание типа из кода не затрагивается, уже сохранённое в поле значение продолжает отрисовываться. | +![Имя Damage ×, иконка и группа Combat/Modifiers в окне выбора](../Images/type-selector-display.png) -`Hidden` пригодится для типа, который присваивается, но не предназначен для настройки в инспекторе — адаптер поверх делегата, тестовая заглушка, базовая реализация, оставленная только для кода: +Имя Damage ×, иконка и группа Combat/Modifiers в окне выбора -```csharp -[TypeSelectorDisplay(Hidden = true)] -public sealed class DelegateModifier : IModifier { } -``` +| Свойство | Результат | +|---|---| +| `Name` | Подпись в списке и закрытом поле. Поиск продолжает находить настоящее имя типа | +| `Group` | Группировка вместо namespace; `/` разделяет уровни, например `Combat/Melee` | +| `Tooltip` | Текст подсказки при наведении | +| `Icon` | Имя `EditorGUIUtility.IconContent`, путь к ассету с расширением или путь в `Resources` без расширения | +| `Hidden` | При `true` скрывает тип из обычного выбора. Не наследуется и не мешает присваиванию из кода или отображению сохранённого значения | -`Hidden` управляет настройкой, а не восстановлением. Пикер **починки** — **Fix** для потерянной ссылки и групповая починка в окне References — скрытые типы по-прежнему предлагает: ссылку, уже сохранённую с таким типом, нужно оставить перенаправляемой. **Smart Fix**, который сам предлагает тип, а не даёт выбрать, скрытые типы не подсказывает никогда. +> [!NOTE] +> `TypeSelectorDisplay` зависит от `UNITY_EDITOR` в сборке, где атрибут применён. Если класс собран во внешнюю DLL без этого символа, настройки атрибута, включая `Hidden`, в неё не попадут. -В пикере `DamageModifier` из примера выше показывается в группе `Combat/Modifiers` как «Damage ×» со своей иконкой — рядом с типами, сохранившими вид по умолчанию: +## TypeSelectorWindow -![Кастомные имя, иконка и группа в пикере через TypeSelectorDisplay](../Images/aspid_fasttools_type_selector_display.png) +Окно группирует типы по namespace или `Group` и различает одинаковые имена по сборкам. Через `TypeSelectorWindow` его можно открыть из своего инспектора или окна редактора. -## TypeSelectorWindow +![Избранные и недавние типы на корневой странице селектора](../Images/type-selector-window.png) + +Избранные и недавние типы на корневой странице селектора + +| Действие | Управление | +|---|---| +| Перемещение / выбор / закрытие | Стрелки / Enter / Escape | +| Возврат в родительскую группу | Стрелка влево или хлебные крошки | +| Переключение избранного | Space или звёздочка при наведении | +| Очистка значения | `` | + +Отображение **Favorites**, **Recent** и ёмкость истории настраиваются во вкладке **Settings** окна FastTools. -Всплывающее окно выбора типа с поиском и иерархией по пространствам имён — тот же пикер, что открывают `[TypeSelector]` и `SerializableType`, доступный и как публичный API. Окно включает: +### Generic-типы -- Иерархическую организацию по пространствам имён -- Текстовый поиск с фильтрацией -- Навигацию с клавиатуры (стрелки, Enter, Escape; Space — в избранное) -- Хлебные крошки и возврат назад (стрелка ← или клик по крошке) -- Разрешение неоднозначности для типов с одинаковыми именами из разных сборок -- Секции **Favorites** (★ при наведении) и **Recent** (последние выборы) на корневой странице — хранятся локально для каждого проекта (`EditorPrefs`, не попадают в репозиторий), скрыты во время поиска -- Пункт `` вверху списка и галочку ✓ у текущего значения — его строка выбирается при открытии -- Счётчики типов у групп и заголовков секций -- Поддержку generic-типов — выбор открытого generic ведёт через выбор его аргументов и возвращает сконструированный тип -- Настройку Favorites/Recent (вкл/выкл, ёмкость Recent) во вкладке Settings окна SerializeReference +При выборе открытого generic-типа окно предлагает выбрать аргументы, а затем возвращает сконструированный закрытый тип. Например, для `Container` после выбора `int` результатом будет `Container`. Generic-аргумент тоже может быть generic-типом: сначала окно попросит задать его собственные аргументы. -![Корневая страница пикера с Favorites, Recent и счётчиками пространств имён](../Images/aspid_fasttools_type_selector_window.png) +![Выбор аргумента generic-типа в селекторе](../Images/type-selector-generic.gif) -Выбор открытого generic проходит через страницу его аргументов и возвращает сконструированный тип: +Выбор аргумента generic-типа в селекторе -![Выбор открытого generic через страницу аргументов](../Images/aspid_fasttools_type_selector_generic.gif) +Аргумент должен удовлетворять ограничениям generic-параметра; `[Serializable]` не требуется. Для `[SerializeReference]` действуют дополнительные [правила сериализуемости и вывода аргументов](03-serialize-reference-selector.md#generic-типы). -> Страница аргументов показывает только типы, которые Unity умеет сериализовать как значение поля: примитивы, `enum`, `string`, ссылки на наследников `UnityEngine.Object` и классы/структуры с `[Serializable]`. Абстрактные типы, интерфейсы, открытые generic и делегаты никогда не попадают в список кандидатов. Чтобы тип стал доступен для выбора, пометьте его атрибутом `[Serializable]`. +### Открытие из кода -Окно доступно как публичный API — открывайте его из любого editor-кода (кастомных инспекторов, `EditorWindow`, пунктов меню), когда нужно вывести выбор типа за пределы стандартного потока `SerializableType` / `[TypeSelector]`. +В editor-скрипте используйте `Aspid.FastTools.Types.Editors`. `screenRect` — прямоугольник кнопки в **экранных координатах**, `selectedTypeName` — строка текущего выбора: ```csharp -namespace Aspid.FastTools.Types.Editors -{ - public sealed class TypeSelectorWindow : EditorWindow +TypeSelectorWindow.Show( + screenRect, + new TypeSelectorFilter { - public static void Show( - Rect screenRect, - TypeSelectorFilter filter = default, - string currentAqn = "", - Action onSelected = null); - } -} + Types = new[] { typeof(MonoBehaviour) }, + Allow = TypeAllow.None + }, + currentAqn: selectedTypeName, + onSelected: aqn => selectedTypeName = aqn); ``` -| Параметр | Описание | -|----------|----------| -| `screenRect` | Прямоугольник в экранных координатах, к которому привязывается dropdown. | -| `filter` | Объединяет, какие типы предлагает селектор: базовые типы (`Types`, в списке остаются только типы, совместимые со **всеми** записями; по умолчанию — `typeof(object)`), включаемые категории (`Allow`), необязательный предикат `Predicate`, дополнительные записи `AdditionalTypes`, предикат аргументов открытых генериков `ArgumentFilter` (какие типы показывает страница аргументов) и `InferredArgumentFilter` (допустим ли для конкретного параметра аргумент, который поле определило само) и `HideNoneOption` (убрать строку ``, когда цель всегда должна хранить тип). | -| `currentAqn` | Assembly-qualified имя текущего выбранного типа: окно сразу откроется на его уровне иерархии. Передайте `null` или пустую строку, чтобы стартовать с корня. | -| `onSelected` | Callback с assembly-qualified именем выбранного типа или `null`, если пользователь выбрал ``. | +Обработчик получает assembly-qualified name или `null` при выборе ``. Закрытие окна без выбора не присваивает значение. Если результат хранится в ассете, записывайте его через `SerializedProperty` и применяйте изменения. -## ComponentTypeSelector +`currentAqn` задаёт текущую отметку; пустая строка отмечает ``, а `null` оставляет выбор без отметки. -Сериализуемая структура, добавляющая в Inspector выпадающий список для смены типа объекта. Добавьте её как поле в базовый класс — при выборе подтипа редактор перезаписывает `m_Script` на `SerializedObject`, фактически превращая компонент или ScriptableObject в выбранный подтип. +### Фильтры окна -Список автоматически ограничивается подтипами класса, в котором объявлено поле. Дополнительная настройка не требуется. +`TypeSelectorFilter` — структура. У `default` значение `Allow` равно `None`, в отличие от атрибута `[TypeSelector]`, где по умолчанию `All`. Задавайте режим явно, когда нужны абстрактные классы или интерфейсы. -```csharp -using UnityEngine; -using Aspid.FastTools.Types; +
+Свойства фильтра окна -public abstract class EnemyBase : MonoBehaviour -{ - [SerializeField] private ComponentTypeSelector _enemyType; - [SerializeField] [Min(0)] private float _health = 100f; +| Свойство | Назначение | +|---|---| +| `Types` | Все базовые типы, которым должен соответствовать кандидат | +| `Allow` | Разрешённые категории: абстрактные классы и интерфейсы | +| `Predicate` | Дополнительное условие после проверки типа и категории | +| `AdditionalTypes` | Кандидаты, обходящие `Types`, `Allow` и `Predicate`; фильтр `Hidden` сохраняется | +| `ArgumentFilter` | Дополнительный фильтр аргументов, выбираемых вручную | +| `InferredArgumentFilter` | Фильтр аргументов, выведенных из типа поля | +| `IncludeHidden` | Показывать типы с `Hidden = true` | +| `HideNoneOption` | Скрыть `` на корневой странице | - public abstract void Attack(); -} +Для сужения списка используйте `Predicate`; `AdditionalTypes` добавляет кандидатов в обход ограничений. -public sealed class FastEnemy : EnemyBase -{ - [SerializeField] [Min(0)] private float _speed = 25f; +
- public override void Attack() => - Debug.Log($"Fast enemy strikes! (speed: {_speed})"); -} -``` +Пример окна, работающего с ассетами, есть в [EditorTools](../../Samples~/EditorTools/Documentation/README.ru.md). + +## Если выбор не работает + +| Симптом | Что проверить | +|---|---| +| Нужный класс отсутствует | Совместимость с базой и всеми ограничениями, `Allow`, `Hidden` и ошибки компиляции | +| В SerializableMonoScript нет типа, который есть в SerializableType | Есть ли отдельный файл скрипта и возвращает ли `MonoScript.GetClass()` нужный класс | +| После смены Category осталось старое значение | Ограничение меняет список кандидатов, а не переписывает зависимое поле | +| `` при непустом имени | Не изменились ли класс, namespace или сборка; выберите существующий тип заново | +| Required не предупреждает о потерянном типе | Для строк и обёрток проверяется пустое имя, а не успешность его разрешения | +| Type выбран, но объект не появился | Хранение типа не создаёт экземпляр; используйте свой код создания или [SerializeReference Selector](03-serialize-reference-selector.md) | + +## Пример в пакете -![ComponentTypeSelector меняет тип компонента в Инспекторе](../Images/aspid_fasttools_component_type_selector.gif) +Выбор типов врагов и паттерна расстановки в инспекторе показан в примере [Types](../../Samples~/Types/Documentation/README.ru.md). -Заметки о поведении дропдауна смены типа: +![Волна обычных и элитных врагов движется к центру.](../../Samples~/Types/Documentation/Images/demo.gif) -- Так как сменой типа управляет сам список, встроенная строка **Script** в Inspector скрывается, пока присутствует селектор — тип меняется только через выпадающий список (только UIToolkit-инспекторы; устаревший IMGUI-инспектор рисует эту строку сам). +Волна обычных и элитных врагов движется к центру. diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/03-serialize-reference-selector.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/03-serialize-reference-selector.md index dc7e2379..1006a7c2 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/03-serialize-reference-selector.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/03-serialize-reference-selector.md @@ -1,93 +1,266 @@ # SerializeReference Selector -Стандартный Inspector не умеет заполнять поля `[SerializeReference]`: managed-ссылку нельзя -создать из UI, а при переименовании или удалении типа Unity молча очищает данные. -SerializeReference Selector закрывает оба пробела: выпадающий выбор реализации прямо -в Инспекторе плюс точечная починка сломанных ссылок у самого поля. Аудит по всему проекту, -массовая починка и build/CI-гейт — в [SerializeReference Tooling](04-serialize-reference-tooling.md). +Выбирайте реализацию интерфейса или базового класса прямо в поле `[SerializeReference]`. Селектор создаёт экземпляр, раскрывает его поля и переносит совместимые данные при смене типа. Здесь — настройка отдельных полей в инспекторе; аудит проекта, массовое восстановление и CI описаны в [SerializeReference Tooling](04-serialize-reference-tooling.md). -**Разделы справочника:** + -* [`Inspector type dropdown`](#inspector-type-dropdown) — дропдаун `[TypeSelector]` - на полях `[SerializeReference]`: выбор реализации, вложенный inspector, generics, - copy/paste; -* [`Repairing broken references`](#repairing-broken-references) — жёлтое предупреждение - вместо молчаливой очистки, **Fix** / **Smart Fix** / **Make unique**. +## Быстрый старт -## Inspector type dropdown +Добавьте `[TypeSelector]` рядом с `[SerializeReference]`: реализацию можно будет выбрать в окне с поиском, без собственного редактора. -Добавьте `[TypeSelector]` рядом с `[SerializeReference]` — Inspector заменит стандартный -UI managed-ссылки иерархическим [окном выбора типа](02-serializable-types.md#typeselectorwindow) с поиском. -Вы прямо в инспекторе выбираете, какая конкретная реализация типа поля будет создана; -`` очищает ссылку. +| Создание в коде | Выбор в инспекторе | +|---|---| +|
[SerializeReference]
private IWeapon _primary = new Pistol();
|
[TypeSelector]
[SerializeReference]
private IWeapon _primary;
| -```csharp -using System; -using UnityEngine; -using System.Collections.Generic; -using Aspid.FastTools.Types; +Селектор хранит **экземпляр с данными**. Если нужно сохранить только имя класса и создать объект позже из кода, используйте [Serializable Type System](02-serializable-types.md). -public interface IWeapon -{ - void Fire(); -} +![Смена Pistol на Shotgun сохраняет Damage = 37 и добавляет поле Pellets](../Images/aspid_fasttools_serialize_reference_selector.gif) + +Смена Pistol на Shotgun сохраняет Damage = 37 и добавляет поле Pellets + +Готовая сцена с оружием, эффектами и вложенными модификаторами есть в [примере SerializeReferences](../../Samples~/SerializeReferences/Documentation/README.ru.md). + +## Настройка выбора + +Тип поля задаёт базовую совместимость: для `IWeapon` предлагаются его реализации, для абстрактного класса — конкретные наследники. Используйте `[Serializable]` на классах, данные которых должны сохраняться Unity. + +| Задача | Как сделать | +|---|---| +| Оставить только оружие ближнего боя | `[TypeSelector(typeof(IMelee))]` на поле `IWeapon`: кандидат должен подходить полю и реализовывать `IMelee` | +| Показывать предупреждение у пустого поля | `[TypeSelector(Required = true)]` | +| Управлять ограничением из другого поля | `[TypeSelector(nameof(_category))]`; пример — [ограничение из другого поля](02-serializable-types.md#ограничение-из-другого-поля) | +| Изменить имя, группу, подсказку или иконку | `[TypeSelectorDisplay(...)]` на классе | +| Скрыть реализацию из обычного выбора | `[TypeSelectorDisplay(Hidden = true)]` | + +`TypeSelector.Allow` на `[SerializeReference]` не используется: селектор создаёт экземпляры конкретных классов. Интерфейсы, абстрактные классы, структуры, `string`, делегаты и наследники `UnityEngine.Object` не подходят в качестве создаваемого значения. + +### Имя и группа в списке +Добавьте атрибут к `Shotgun` из примера: + +```csharp [Serializable] -public sealed class Pistol : IWeapon +[TypeSelectorDisplay( + Name = "Дробовик", + Group = "Оружие/Дальнее", + Tooltip = "Оружие с несколькими дробинами")] +public sealed class Shotgun : IWeapon { - [SerializeField] [Min(0)] private int _damage = 10; + [SerializeField, Min(0)] private int _damage = 20; + [SerializeField, Min(1)] private int _pellets = 6; - public void Fire() => Debug.Log($"Pistol: {_damage} dmg"); + public void Fire() => Debug.Log($"Shotgun: {_damage} dmg, {_pellets} pellets"); } +``` + +Класс появится как **Оружие → Дальнее → Дробовик**. Поиск продолжит находить его по настоящему имени `Shotgun`. Эти подписи не переименовывают сохранённый тип. + +`Hidden = true` скрывает тип из обычного выбора, но уже назначенное значение продолжает отображаться, а присваивание из кода остаётся доступным. Настройка не наследуется подклассами. Полный список параметров — в [TypeSelectorDisplay](02-serializable-types.md#typeselectordisplay). + +### Обязательное поле + +```csharp +[TypeSelector(Required = true)] +[SerializeReference] private IWeapon _primary; +``` + +У пустого поля появится **Required reference is not set**. Атрибут не создаёт значение сам и не запрещает выбрать ``; проверку `null` в игровом коде он тоже не заменяет. Потерянный тип диагностируется отдельно от незаполненного поля. + +Для проверки обязательных полей в CI включите [`-srGateRequired`](04-serialize-reference-tooling.md#запуск-в-ci). Обычная проверка перед сборкой ищет потерянные типы; границы проверки `Required` описаны в [SerializeReference Tooling](04-serialize-reference-tooling.md#где-проверяются-обязательные-поля). + +## Списки и вложенные ссылки -public sealed class Loadout : MonoBehaviour +Для массива или списка атрибуты ставятся на поле коллекции. В одном списке могут находиться разные реализации и `null`. + +```csharp +// Дополнительно: using System.Collections.Generic; + +[TypeSelector] +[SerializeReference] private List _sidearms = new(); + +[TypeSelector] +[SerializeReference] private IWeapon[] _slots = new IWeapon[2]; +``` + +В списке UI Toolkit кнопка **+** открывает выбор типа и добавляет новый экземпляр. Выбор `` добавляет пустой элемент. Для такого же добавления в собственном IMGUI-инспекторе используйте `SerializeReferenceIMGUIList.Draw` — [пример ниже](#собственный-imgui-инспектор). + +### Вложенный селектор без повторения атрибута + +Внутреннее поле `[SerializeReference]` получает селектор автоматически. Например, добавьте к примеру оружие, которое оборачивает другое оружие: + +```csharp +[Serializable] +public sealed class DoubleShot : IWeapon { - [TypeSelector] - [SerializeReference] private IWeapon _primary; + [SerializeReference] public IWeapon Weapon; - [TypeSelector] - [SerializeReference] private List _sidearms; + public void Fire() + { + Weapon?.Fire(); + Weapon?.Fire(); + } } ``` -Атрибут существует только в редакторе (`[Conditional("UNITY_EDITOR")]`) и не несёт -стоимости в рантайме. Работает с одиночными полями, массивами и `List`, в инспекторах -IMGUI и UIToolkit. Тот же атрибут работает и с полями `string` и `SerializableType` — -см. [TypeSelectorAttribute](02-serializable-types.md#typeselectorattribute). +Выберите **DoubleShot** в `Primary`, затем **Pistol** в его поле **Weapon**. Повторять `[TypeSelector]` у `Weapon` не нужно. Так же обрабатываются вложенные массивы и списки managed-ссылок. + +Автоматическая отрисовка охватывает восемь уровней вложенности, после чего используется стандартная отрисовка Unity. Это ограничение отрисовки, а не запрет на хранение более глубокого графа. Если дочернее поле уже имеет `[TypeSelector]` или собственный `[CustomPropertyDrawer]`, его отрисовка сохраняется. + +## Работа с данными -![Выбор реализации в managed-ссылке: пикер и вложенный inspector выбранного экземпляра](../Images/aspid_fasttools_serialize_reference_selector.gif) +### Что происходит при смене типа -| Возможность | Что делает | +Селектор создаёт экземпляр выбранного класса и пытается перенести данные предыдущего. Для примера из быстрого старта результат такой: + +| Поле | Pistol до смены | Shotgun после смены | +|---|---|---| +| `_damage` | `37` | `37`: совпадают имя и форма данных | +| `_pellets` | Отсутствует | `6`: начальное значение нового экземпляра | + +Перенос рассчитан на совместимые сериализуемые поля. Переименованные поля и несовместимые структуры данных требуют отдельной миграции. Поля, которых нет в новом типе, не хранятся «про запас»: если настроить **Pellets = 12**, перейти на `Pistol`, а затем снова на `Shotgun`, **Pellets** станет `6`. + +Совпадающие по имени и совместимые вложенные поля `[SerializeReference]` переносятся с сохранением существующих экземпляров. Смена внешнего типа сама по себе не делает их независимыми копиями. + +
+Начальные значения и конструктор + +При создании вызывается конструктор без параметров, в том числе непубличный. Если такого конструктора нет, экземпляр создаётся без вызова конструктора: полагаться на инициализаторы полей в этом случае нельзя. Для предсказуемых начальных значений оставьте классу конструктор без параметров. + +
+ +### Copy / Paste и шаблоны + +Правый клик по **заголовку поля ссылки** открывает контекстное меню. + +| Действие | Результат | |---|---| -| **Выбор реализации** | В списке — конкретные классы (не наследники `UnityEngine.Object`), совместимые с типом поля. `[TypeSelector(typeof(IMelee))]` сужает список до реализаций `IMelee`, а `[TypeSelectorDisplay(Hidden = true)]` убирает из пикера отдельный тип. | -| **Open generics** | `Modifier` и подобные: аргументы выводятся из поля — в том числе через реализуемые им интерфейсы, поэтому поле `IConverter` сразу закрывает кандидата `Sequence : IConverter`, — либо выбираются на второй странице селектора, если поле оставляет параметр неопределённым. Определённый кандидат показывается в списке закрытым (`Sequence`), так что строка называет то, что создастся. Кандидат, которого не закрыть под поле ни одним аргументом, не показывается вовсе — `ToString : IConverter` отсутствует у поля `IConverter`, — при этом объявленная вариантность учитывается, поэтому для `IConverter` он остаётся. Аргумент обязан быть сериализуемым только там, где кандидат его хранит: кандидат, держащий `T` за `[SerializeReference]`, закрывается любым `T`, — а страница аргументов по-прежнему предлагает только сериализуемые типы. | -| **Вложенные ссылки** | Поле `[SerializeReference]` (или массив/список) *внутри* назначенного экземпляра получает тот же дропдаун, поэтому граф настраивается на всю глубину без аннотаций на каждом уровне — на 8 уровней, дальше отрисовку снова ведёт Unity. Дочернее поле, для которого у Unity уже есть drawer (собственный `[TypeSelector]` или зарегистрированный для его типа `[CustomPropertyDrawer]`), этот drawer сохраняет. | -| **Сохранение данных** | При смене типа поля, совпадающие по имени и сериализуемой форме, переносятся, а не сбрасываются в значения по умолчанию. | -| **Copy / Paste** | Правый клик по заголовку копирует значение и вставляет его независимым экземпляром в любое совместимое поле. | -| **Мультивыделение** | Смешанное выделение показывает смешанное состояние dropdown; выбор или вставка применяется к каждому объекту в одной группе Undo. | -| **Проверка компилятором** | Анализатор Roslyn: `AFT0004` (ошибка) — тип наследует `UnityEngine.Object`; `AFT0005` (предупреждение) — селектор оказался бы пустым. | - -Пустое поле с `[TypeSelector(Required = true)]` показывает предупреждение «required» -в инспекторе и считается нарушением для -[build/CI-гейта](04-serialize-reference-tooling.md#project-settings--the-buildci-gate) — -см. свойство `Required` в [TypeSelectorAttribute](02-serializable-types.md#typeselectorattribute). - -## Repairing broken references - -Когда сохранённый в ассете тип перестаёт резолвиться или два поля незаметно делят -один экземпляр, селектор не молчит — каждая проблема получает заметку в инспекторе -и кнопку починки рядом: - -| Случай | Решение | +| **Copy Serialize Reference** | Запоминает тип и сериализуемые данные текущего значения | +| **Paste Serialize Reference** | Создаёт новый экземпляр в совместимом поле; учитывает его тип и дополнительные ограничения | +| **Save as Template…** | Сохраняет текущее значение под именем | +| **Paste Template → имя** | Создаёт экземпляр из подходящего сохранённого шаблона | + +Копирование пустой ссылки тоже имеет смысл: следующая вставка очистит целевое поле. При мультивыделении Copy берёт значение первого объекта; выбор типа и Paste создают независимый экземпляр для каждого объекта в одной группе Undo. При смене типа данные переносятся из собственного предыдущего значения каждого объекта. Уведомления `Required`, `Missing type` и `Shared reference` проверяйте при выборе одного объекта. + +> [!NOTE] +> Буфер обмена и шаблоны переносят данные через `JsonUtility`; вложенный граф `[SerializeReference]` этим способом не копируется целиком. Для отделения общей ссылки вместе с её вложенными managed-ссылками используйте **Make unique**. + +Шаблоны хранятся локально в `EditorPrefs` для текущего проекта: это личные заготовки, они не передаются команде через Git. Сохранение под существующим именем запрашивает подтверждение замены. + +### Другие действия в заголовке + +- **Перетащить `.cs` из Project** — назначить экземпляр совместимого класса скрипта. Данные переносятся по тем же правилам, что и при выборе типа. +- **Find Usages of …** — найти использования текущего типа в проекте. +- **Create New Script…** — сохранить заготовку сериализуемого класса, совместимого с объявленным типом поля. После успешной компиляции селектор назначает новый экземпляр. Заготовку нужно дополнить логикой; методы интерфейса могут содержать `NotImplementedException`, а абстрактные члены базового класса потребуется реализовать вручную. + +## Общие ссылки и Make unique + +Два поля одного компонента или `ScriptableObject` могут указывать на один экземпляр. Изменение его данных через любое из полей видно в обоих местах; селектор помечает такую связь как **Shared reference**. Общая ссылка может быть намеренной. + +Чтобы создать её, откройте контекстное меню целевого поля и выберите **Link to Existing → тип и путь**. Меню предлагает подходящие по типу поля ссылки внутри того же объекта-владельца. Это связывание с существующим экземпляром; прежнее значение целевого поля заменяется. + +![Действие Make unique создаёт независимую копию общей ссылки](../Images/aspid_fasttools_serialize_reference_make_unique.png) + +Действие Make unique создаёт независимую копию общей ссылки + +Нажмите **Make unique** в уведомлении или **Make Unique Reference** в контекстном меню, чтобы редактировать поле независимо. Копируются также вложенные managed-ссылки; повторные ссылки внутри самой копии сохраняют общность. + +Автоматическое разделение ссылок после дублирования элемента списка управляется настройкой **Auto de-alias duplicated list elements** в [настройках FastTools](04-serialize-reference-tooling.md#проверка-перед-сборкой). Она включена по умолчанию. + +## Generic-типы + +Селектор выводит аргументы generic-кандидата из типа поля, когда это возможно. Если часть аргументов неизвестна, окно предлагает выбрать их на следующей странице. + +```csharp +public interface IModifier { } + +[Serializable] +public class Modifier : IModifier +{ + public T Value; +} + +// T уже известен: создаётся Modifier. +[TypeSelector] +[SerializeReference] private Modifier _damageModifier; + +// Для Modifier потребуется выбрать T в окне селектора. +[TypeSelector] +[SerializeReference] private IModifier _modifier; +``` + +Интерфейс и класс объявите рядом с остальными типами, а поля добавьте в `Loadout`. Для первого поля аргумент — `float`; для второго можно выбрать, например, `int` или `string` на странице аргументов. + +
+Вывод через интерфейсы и ограничения аргументов + +Аргументы выводятся и через реализуемые интерфейсы: поле `IConverter` закрывает кандидат `Sequence : IConverter` как `Sequence`. + +Кандидат исключается, если его нельзя закрыть под тип поля. Например, `ToString : IConverter` не подходит полю `IConverter`. Если выходной параметр `IConverter` объявлен ковариантным, он может подойти полю `IConverter`. + +Аргумент, выведенный из поля, обязан быть сериализуемым как значение только там, где кандидат хранит его как значение. Параметр за `[SerializeReference]` проверяется по правилам managed-ссылок. Страница ручного выбора предлагает сериализуемые типы. + +
+ + + +## Восстановление потерянного типа + +После переименования, переноса или удаления класса сохранённое имя может перестать разрешаться. У поля появляется **Missing type**. Пока данные ссылки остаются в ассете, их можно переназначить существующей реализации. + +![Потерянная ссылка с действиями Fix и Smart Fix в инспекторе](../Images/aspid_fasttools_serialize_reference_repair.png) + +Потерянная ссылка с действиями Fix и Smart Fix в инспекторе + +| Действие | Когда использовать | |---|---| -| **Потерянный тип** (переименован или удалён) | Жёлтое предупреждение вместо молчаливой очистки. Подчёркнутое **Fix** открывает селектор и переназначает тип с сохранением данных — на любой глубине, в сохранённых ассетах и прямо в Prefab Mode. | -| **Smart Fix** | Рядом с **Fix** предлагает наиболее вероятную замену (`[MovedFrom]`, другой namespace/сборка, регистр, близкое имя) и применяет в один клик — никогда не автоматически. | -| **Общая ссылка** (два поля делят экземпляр) | Помечается лейблом; **Make unique** расщепляет её в независимую копию. Дублирование элемента списка (Ctrl+D, `+`) больше не создаёт алиас. | +| **Fix** | Вы знаете подходящую замену: откройте выбор и назначьте существующий тип | +| **Smart Fix** | Хотите применить предложенный вариант: проверьте тип и причину в подсказке, затем нажмите на предложение | + +Smart Fix учитывает `[MovedFrom]`, имя, namespace, сборку и сходство полей. Это подсказка, которая применяется только по нажатию. Окно **Fix** допускает и скрытые через `Hidden` типы: восстановление старых данных может требовать реализации, убранной из обычного выбора. + +Для ассета на диске Fix переписывает сохранённую запись типа и переимпортирует ассет; обычного Undo у такой записи в файл нет. В открытой сохранённой сцене и Prefab Mode восстановление применяется к объекту в памяти — после проверки результата сохраните сцену или префаб. Сохранение данных не означает автоматического преобразования несовместимых полей; восстановление в памяти также не гарантирует полного восстановления вложенного графа. + +Если Fix недоступен, выберите один объект и проверьте, что сцена или Prefab Mode сохранены и не имеют несохранённых изменений. Для экземпляра префаба в сцене откройте исходный префаб. Если проблема находится внутри потерянного родителя и поле недоступно, используйте [Asset References](04-serialize-reference-tooling.md#asset-references-разобрать-один-ассет). + +Плановое переименование лучше сопровождать [`[MovedFrom]`](04-serialize-reference-tooling.md#миграции-с-movedfrom). Для проверки и восстановления нескольких ассетов переходите к [SerializeReference Tooling](04-serialize-reference-tooling.md). + +## Собственный IMGUI-инспектор + +Селектор работает в IMGUI и UI Toolkit. В собственном IMGUI-редакторе обычный `PropertyField` использует drawer поля, но для кнопки **+** с выбором типа у списка нужен `SerializeReferenceIMGUIList.Draw`. + +Для `Loadout` с полем `_sidearms` из примера выше поместите этот редактор в папку `Editor`: + +```csharp +using UnityEditor; +using UnityEngine; +using Aspid.FastTools.SerializeReferences.Editors; + +[CustomEditor(typeof(Loadout))] +public sealed class LoadoutEditor : Editor +{ + public override void OnInspectorGUI() + { + serializedObject.Update(); + + EditorGUILayout.PropertyField( + serializedObject.FindProperty("_primary"), true); + + SerializeReferenceIMGUIList.Draw( + serializedObject.FindProperty("_sidearms"), + new GUIContent("Sidearms"), + typeof(IWeapon)); + + serializedObject.ApplyModifiedProperties(); + } +} +``` + +Остальные поля добавьте в редактор по мере необходимости. Для создания контролов без `[TypeSelector]` доступны `SerializeReferenceEditorGUI.CreateField`, `CreateList` и `DrawFieldLayout`; готовый редактор есть в [примере SerializeReferences](../../Samples~/SerializeReferences/Documentation/README.ru.md#путь-imgui). -![Заметка Missing type с кнопками Fix и Smart Fix на сломанной managed-ссылке](../Images/aspid_fasttools_serialize_reference_repair.png) +## Если нужного типа нет в списке -![Заметка Shared reference с действием Make unique на двух полях, делящих один экземпляр](../Images/aspid_fasttools_serialize_reference_make_unique.png) +Проверьте, что класс конкретный, совместим с типом поля и дополнительными ограничениями, не наследует `UnityEngine.Object` и не помечен `Hidden = true`. Для generic-кандидата должны существовать допустимые аргументы. После ошибок компиляции дождитесь успешной перекомпиляции скриптов. -Про аудит и массовую починку по всему проекту — см. -[Bulk repair tabs](04-serialize-reference-tooling.md#bulk-repair-tabs). +Анализатор `AFT0004` сообщает о несовместимости с `UnityEngine.Object`, `AFT0005` предупреждает о потенциально пустом селекторе. Параметр `Allow` не расширяет список создаваемых managed-ссылок. +Атрибуты `[TypeSelector]` и `[TypeSelectorDisplay]` применяются только в редакторе. Сами реализации и их сериализованные данные остаются частью игры. diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/04-serialize-reference-tooling.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/04-serialize-reference-tooling.md index 444e6a4a..c500ae68 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/04-serialize-reference-tooling.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/04-serialize-reference-tooling.md @@ -1,78 +1,227 @@ # SerializeReference Tooling -[Селектор в Инспекторе](03-serialize-reference-selector.md) чинит ссылки по одному полю; этот -документ — про проектный масштаб: вкладки окна FastTools для аудита и массовой починки -managed-ссылок, страница Project Settings с гейтом на сборку плеера и та же проверка -в headless-CI. Гейт покрывает и незаданные поля `[TypeSelector(Required = true)]` — -см. свойство `Required` в [TypeSelectorAttribute](02-serializable-types.md#typeselectorattribute). +Находите потерянные `[SerializeReference]` в префабах, ассетах и сценах, восстанавливайте типы группами и проверяйте проект перед сборкой. Окна FastTools читают сохранённые ссылки из YAML, включая записи, которые обычный инспектор уже не показывает. -**Разделы справочника:** + -* [`Bulk repair tabs`](#bulk-repair-tabs) — вкладки **Asset References** и - **Project References** для аудита и массовой починки по всему проекту; -* [`Project settings & the build/CI gate`](#project-settings--the-buildci-gate) — - настройки в Project Settings, их scope и гейт на сборку плеера; -* [`Headless CI`](#headless-ci) — `SerializeReferenceCiGate.RunCheck` для batchmode-пайплайнов. +## Быстрый старт -## Bulk repair tabs +После переименования или удаления класса проверьте, какие ассеты всё ещё хранят его старое имя: -[Чинить ссылки по одной](03-serialize-reference-selector.md#repairing-broken-references) необязательно: -аудит и массовая починка вынесены в отдельные вкладки окна FastTools. +1. Сохраните изменённые сцены и ассеты — поиск читает файлы на диске. +2. Откройте **Tools → Aspid 🐍 → FastTools → Project References**. +3. Нажмите **Scan Project**. Потерянные ссылки сгруппируются по сохранённому типу. +4. В нужной группе нажмите **Fix all**, выберите тип замены и проверьте список изменений в диалоге **Rewrite**. +5. Проверьте итог и значения в затронутых ассетах. Если нужна отмена, используйте **Undo** в итоге операции; затем нажмите **Rescan** для повторной проверки. -| Вкладка | Назначение | +При перезаписи файлов ссылки в открытых сценах и Prefab Mode пропускаются. Перед восстановлением сохраните и закройте эти сцены или префабы либо исправьте видимое поле через [Fix в инспекторе](03-serialize-reference-selector.md#восстановление-потерянного-типа). + +> [!NOTE] +> Для анализа нужны текстовые YAML-ассеты. В настройках редактора Unity выберите **Asset Serialization → Mode → Force Text**. Существующие двоичные ассеты потребуется пересохранить; переключение режима само по себе не делает их доступными для сканирования. + + + +## Окна аудита и восстановления + +| Задача | Инструмент | +|---|---| +| Исправить одно видимое поле | [Селектор в инспекторе](03-serialize-reference-selector.md#восстановление-потерянного-типа) | +| Посмотреть связи внутри одного сохранённого ассета | **Asset References** | +| Найти потерянный тип сразу во всём проекте | **Project References** | +| Записать новое имя типа после `[MovedFrom]` | **Migrate all** в Project References | +| Проверять данные перед сборкой | **Build / CI gate** в настройках проекта | + +Обе вкладки открываются через **Tools → Aspid 🐍 → FastTools**. Проектный поиск обрабатывает `.prefab`, `.asset` и `.unity` под `Assets/`, учитывая **Excluded scan folders**. Он охватывает подходящие файлы проекта, а не только сцены, включённые в сборку. + +## Project References: восстановить группу + +Карточка группы показывает сохранённый тип, число ссылок и файлов. Под ней находятся пути ассетов и идентификаторы ссылок `rid`. Нажмите на строку, чтобы перейти к её ассету в **Asset References**: + +![Группа потерянных ссылок с действиями Fix all и Smart Fix](../Images/aspid_fasttools_serialize_reference_project_references.png) + +Группа потерянных ссылок с действиями Fix all и Smart Fix + +### Какое действие выбрать + +| Действие | Что происходит | +|---|---| +| **Fix all** | Открывает выбор типа; выбранная замена применяется ко всем доступным для записи ссылкам этой группы | +| **Smart Fix** | Использует предложенный тип и открывает подтверждение замены | +| **Migrate all** | Перезаписывает старое имя на тип, однозначно найденный через `[MovedFrom]` | +| **Reassign all** | Позволяет выбрать другую замену для группы, распознанной как миграция | + +**Smart Fix** появляется, когда найден подходящий кандидат по данным о типе, сходству имени и полям. Это предложение, которое нужно проверить; само сканирование ничего не исправляет. + +### Что сохраняется при восстановлении + +При замене потерянного типа инструмент меняет его запись `class`, `ns` и `asm` в YAML, сохраняя блок данных и `rid`. Затем Unity повторно импортирует файл и читает данные уже как новый тип. + +Выбирайте тип, совместимый с объявленным типом поля и сохранёнными данными. Замена имени не преобразует структуру полей автоматически. Если группа объединяет разные типы полей, диалог предупреждает об этом: выбранный класс может подойти не всем записям, и несовместимые ссылки станут `null` при импорте. + +После массовой замены появляется итог с кнопкой **Undo**. Она возвращает прежнее имя типа у ссылок, которые всё ещё содержат применённую замену. Это действие из итога операции, а не восстановление всех прежних значений ассета. Проверьте результат до нового сканирования: **Rescan** очищает итоги предыдущих операций. + +### Если выбрать None + +`` очищает ссылки и удаляет их сохранённые данные. Если несколько полей используют один `rid`, очищаются все указатели на этот экземпляр. Инструмент запрашивает подтверждение; у этой операции нет отмены. + +При массовой очистке ссылки из открытых сцен или Prefab Mode могут быть обнулены в памяти. Сохраните эти объекты: до сохранения поиск по файлам продолжит показывать прежние записи. + +## Asset References: разобрать один ассет + +Откройте **Asset References** и укажите сохранённый префаб, ScriptableObject или файл сцены в поле объекта рядом с **Rescan**. Можно также перейти сюда из строки результата **Project References**. + +Граф показывает ссылки по объектам-владельцам и путям полей: + +| Обозначение | Значение | +|---|---| +| **MISSING** | Сохранённый тип ссылки не найден | +| **SHARED** | Несколько полей используют один экземпляр managed-ссылки | +| **Orphaned** | В YAML осталась запись, на которую не указывает поле | +| `rid` | Идентификатор managed-ссылки внутри её объекта-владельца | + +`SHARED` само по себе не означает ошибку: общая ссылка может быть намеренной. Одинаковый цвет помогает найти связанные поля; цвет определяется идентификатором, отдельной настройки для него нет. + +На карточке потерянной ссылки откройте **Fix** и выберите новый тип. В примере ниже `GhostWeapon` заменяется на `Pistol`: + +![Восстановление GhostWeapon как Pistol с сохранением данных ссылки](../Images/aspid_fasttools_serialize_reference_tooling.gif) + +Восстановление GhostWeapon как Pistol с сохранением данных ссылки + +Для перезаписи YAML сцена или префаб должны быть закрыты. Если обычное поле нельзя изменить из этого окна — например, оно находится в сцене или под потерянной родительской ссылкой — исправьте родителя либо откройте поле в инспекторе. + +У осиротевшей записи есть **Clear**. Эта команда удаляет запись из файла после подтверждения и не поддерживает Undo. + +## Миграции с MovedFrom + +Если вы намеренно переименовали класс или перенесли его, `[MovedFrom]` связывает старое имя с новым типом. Например, при переименовании `GhostWeapon` в `Pistol` в той же сборке и пространстве имён: + +| До — GhostWeapon | После — Pistol | |---|---| -| **Asset References** (`Tools → Aspid 🐍 → FastTools → Asset References`) | Строит весь граф managed-ссылок ассета прямо из YAML — дерево по компонентам с путями полей, общими и осиротевшими ссылками, значками `MISSING` / `SHARED` и инлайн-выбором типа на каждой карточке. Достаёт потерянные ссылки, которые инспектор не показывает. | -| **Project References** (`Tools → Aspid 🐍 → FastTools → Project References`) | `Scan Project` обходит каждый `.prefab` / `.asset` / `.unity` под `Assets/`, группирует сломанные ссылки по сохранённому типу и чинит всю группу одним `Fix all` (плюс Smart Fix). Группа, чей сохранённый тип совпадает с объявленным переименованием `[MovedFrom]`, читается как ожидающая миграция, а не поломка — один клик **Migrate all** запекает переименование в файлы, после чего атрибут можно удалить из кода. | +|
[Serializable]
public sealed class GhostWeapon
{
    public int Damage = 10;
}
|
[Serializable]
[MovedFrom(true,
    sourceClassName: "GhostWeapon")]
public sealed class Pistol
{
    public int Damage = 10;
}
| + +Для атрибутов нужны `using System;` и `using UnityEngine.Scripting.APIUpdating;`. При переносе также укажите прежние `sourceNamespace` и `sourceAssembly`. -Вкладка **Asset References** раскладывает граф managed-ссылок одного ассета по карточкам -со значками `MISSING` / `SHARED` и инлайн-починкой: +После компиляции: -![Вкладка Asset References: граф ссылок ассета с карточкой Fix Missing](../Images/aspid_fasttools_serialize_reference_asset_references.png) +1. Нажмите **Scan Project** или **Rescan**. +2. Если старое имя однозначно связано с подходящим типом, группа отобразится как ожидающая миграция. +3. Нажмите **Migrate all**, чтобы записать новое имя в файлы. Пока имя остаётся старым, Unity использует атрибут при загрузке. -Вкладка **Project References** группирует находки всего проекта по сохранённому типу — -одна группа чинится целиком одним `Fix all`: +Ожидающая миграция не считается потерянным типом для проверки сборки. Если одно старое имя заявлено несколькими типами, инструмент не выбирает победителя автоматически. Сохранённые закрытые generic-типы также не распознаются этим механизмом как однозначная миграция. -![Вкладка Project References: группа сломанных ссылок с Fix all и Smart Fix](../Images/aspid_fasttools_serialize_reference_project_references.png) +Удаляйте `[MovedFrom]` только после миграции всех данных, которые должны продолжать загружаться, включая ассеты вне текущего проекта и исключённые из сканирования папки. -## Project settings & the build/CI gate + -**`Project Settings → Aspid FastTools → SerializeReference`** содержит: +## Проверка перед сборкой -| Настройка | Scope | Что делает | +Откройте **Project Settings → Aspid FastTools → SerializeReference** и задайте **Build / CI gate**: + +| Режим | Сборка плеера | Отдельный CI-запуск | |---|---|---| -| **Breakage detection** | per-user | Проактивный тост + предупреждение в Console, когда ссылки заново становятся потерянными после рекомпиляции / импорта. | -| **Auto de-alias duplicated list elements** | коммитимая | Дублированный элемент списка получает собственный экземпляр вместо совместного использования id оригинала. | -| **Build / CI gate** | коммитимая | `Off` / `Warn` / `Fail`: при сборке плеера логировать или прерывать сборку на потерянных (а для CI — и на незаданных обязательных) managed-ссылках. | -| **Excluded scan folders** | коммитимая | Пути, пропускаемые при всех проектных сканах. | +| `Off` | Проверка пропускается | Сканирование и запись отчёта пропускаются, код `0` | +| `Warn` | Предупреждение, сборка продолжается | Нарушения записываются в журнал, код `0` | +| `Fail` | Потерянные типы прерывают сборку | При обнаруженных нарушениях код `1` | -- Коммитимые значения хранятся в `ProjectSettings/SerializeReferenceSharedSettings.asset` — закоммитьте его, чтобы команда и CI вели себя одинаково; breakage detection остаётся per-machine (`EditorPrefs`). -- Rid colours — не настройка: общая ссылка всегда раскрашивается по id — совпадающий цвет и показывает, какие поля делят один экземпляр. +Исходный режим — `Warn`. Настройка регулирует проверку, а не выполняет восстановление ссылок. -Те же опции продублированы во вкладке **Settings** окна (`Tools → Aspid 🐍 → FastTools → Settings`) и на странице **`Preferences → Aspid FastTools`**, рядом с индивидуальными настройками пикера: +### Где проверяются обязательные поля -- **Favorites** — переключатель секции. -- **Recent items** — слайдер ёмкости (0–20; 0 скрывает секцию и приостанавливает запись, не стирая историю). -- **Saved lists** — очищает сохранённые Favorites / Recent. -- **Welcome** — переключатель автопоказа. +| Запуск | Потерянные типы | Пустые поля с `TypeSelector(Required = true)` | +|---|---|---| +| **Project References → Scan Project** | Да, включая группы ожидающих миграций | Да при режиме `Warn` или `Fail`; отдельная группа **Required violations** | +| Сборка плеера | Да при режиме `Warn` или `Fail` | Нет | +| CI без `-srGateRequired` | Да, если проверка включена | Нет | +| CI с `-srGateRequired` | Да, если проверка включена | Да | + +Для обязательных полей учитываются ограничения обхода, описанные в разделе [Запуск в CI](#запуск-в-ci). Проектное сканирование потерянных типов доступно и при режиме `Off`; выключена именно дополнительная проверка Required. + +### Общие и личные настройки + +| Настройка | Где хранится | Назначение | +|---|---|---| +| **Build / CI gate** | В проекте | Строгость проверки | +| **Excluded scan folders** | В проекте | Папки, пропускаемые проектными сканированиями | +| **Auto de-alias duplicated list elements** | В проекте | Независимая копия при дублировании элемента списка | +| **Breakage detection** | Локально, `EditorPrefs` | Уведомление и предупреждение в Console о новых потерянных ссылках после импорта или перекомпиляции | -Каждая строка помечена полоской scope (зелёная — коммитимые, синяя — индивидуальные); закреплённый футер предлагает **Reset to defaults** отдельно для каждого scope (сохранённые списки Favorites / Recent сброс переживают). Все поверхности зеркалят друг друга живьём. +Общие настройки сохраняются в `ProjectSettings/SerializeReferenceSharedSettings.asset`. Добавьте файл в систему контроля версий, чтобы команда и CI использовали одинаковые правила. -## Headless CI +
+Другие настройки окна и селектора -Для headless-CI та же проверка запускается методом `SerializeReferenceCiGate.RunCheck`: -он сканирует проект, пишет отчёт, логирует каждое нарушение и учитывает коммитимую -строгость гейта — `Off` пропускает проверку, `Warn` логирует, но завершается с кодом 0, -`Fail` завершается с кодом 1 при нарушениях (код 2 — внутренняя ошибка самой проверки). +Те же параметры доступны во вкладке **Settings** окна FastTools и в **Preferences → Aspid FastTools**. Рядом находятся личные настройки: + +- **Favorites** — отображение избранного. +- **Recent items** — ёмкость истории от 0 до 20. Значение 0 скрывает секцию и приостанавливает запись, сохраняя историю. +- **Saved lists** — очистка Favorites и Recent. +- **Welcome** — автоматический показ приветствия. + +Зелёная полоска обозначает настройки проекта, синяя — личные. **Reset to defaults** сбрасывает каждую группу отдельно и сохраняет списки Favorites и Recent. Изменения сразу отражаются во всех представлениях настроек. + +
+ + + +## Запуск в CI + +Запустите редактор Unity из корня Unity-проекта. `Unity` здесь обозначает исполняемый файл редактора; если его нет в `PATH`, укажите полный путь. ```bash Unity -batchmode -quit -projectPath . \ -executeMethod Aspid.FastTools.SerializeReferences.Editors.SerializeReferenceCiGate.RunCheck \ - -srGateReport SerializeReferenceGateReport.txt -srGateRequired + -srGateReport SerializeReferenceGateReport.txt \ + -srGateRequired -srGateFail ``` -| Флаг | Описание | +Команда проверяет потерянные типы и незаполненные обязательные поля, создаёт отчёт и завершает процесс с кодом `1` при нарушениях. `-srGateFail` явно включает строгий режим, даже если в проекте выбран `Off`. Сам запуск проверки не исправляет ассеты. + +### Флаги запуска + +| Флаг | Действие | |---|---| -| `-srGateReport ` | Путь файла отчёта; по умолчанию `SerializeReferenceGateReport.txt` в корне проекта. Каждое нарушение — машиночитаемая строка с типом нарушения, путём ассета и путём поля. | -| `-srGateRequired` | Дополнительно проверяет незаданные поля `[TypeSelector(Required = true)]` в префабах, ScriptableObject и сценах (required-поля верхнего уровня, чистый YAML-проход). | -| `-srGateWarnOnly` | Переопределяет коммитимую строгость на `Warn` для этого запуска: нарушения логируются, но код выхода 0. Выигрывает у `-srGateFail`, если переданы оба. | -| `-srGateFail` | Переопределяет коммитимую строгость на `Fail` для этого запуска: код выхода 1 при нарушениях. | +| `-srGateReport ` | Путь отчёта; по умолчанию `SerializeReferenceGateReport.txt` | +| `-srGateRequired` | Дополнительно проверить незаполненные поля с `Required = true` | +| `-srGateFail` | Использовать `Fail` вместо настройки проекта | +| `-srGateWarnOnly` | Использовать `Warn`; побеждает `-srGateFail`, если переданы оба | + +Без флага строгости используется настройка проекта. Для ознакомительного запуска замените `-srGateFail` на `-srGateWarnOnly`. + +В режиме `Warn` нарушения логируются как ошибки, но процесс возвращает `0`. В CI проверяйте код выхода. В режиме `Off` новый отчёт не записывается: файл от предыдущего запуска может остаться на диске. + +### Границы проверки Required + +В префабах и ScriptableObject проверка идёт через сериализованные свойства, включая доступные вложенные поля. Сцены читаются из YAML: проверяются поля верхнего уровня и поля внутри контейнеров, сериализуемых по значению. Обход сцен не спускается в элементы коллекций и внутрь managed-ссылок. + +Это ограничение относится к поиску пустых обязательных полей. Поиск потерянных типов отдельно читает сохранённые записи managed-ссылок. + +### Отчёт и коды выхода + +После заголовка каждое нарушение занимает одну строку. Поля разделены табуляцией: + +```text +KIND assetPath fileId rid className fieldPath +``` + +| Поле | Содержимое | +|---|---| +| `KIND` | `MissingType` или `RequiredUnset` | +| `assetPath` | Путь файла, например `Assets/Weapons/Pistol.prefab` | +| `fileId` | Идентификатор объекта-владельца внутри файла | +| `rid` | Идентификатор managed-ссылки; для обязательного строкового поля — `0` | +| `className` | Сохранённое имя класса для `MissingType`, без отдельных полей namespace и сборки | +| `fieldPath` | Путь обязательного поля; для `MissingType` остаётся пустым | + +Сохраните отчёт как артефакт CI. Сочетание пути ассета, `fileId` и `rid` помогает найти конкретную запись в Asset References. + +| Код | Значение | +|---|---| +| `0` | Нарушений нет, выбран `Warn` или проверка отключена | +| `1` | Найдены нарушения в режиме `Fail` | +| `2` | Внутренняя ошибка проверки, например не удалось записать отчёт | + +## Продолжить + +- [SerializeReference Selector](03-serialize-reference-selector.md) — выбор типа, общие ссылки и восстановление отдельного поля в инспекторе. +- [Serializable Types](02-serializable-types.md) — `TypeSelector` и настройка обязательных полей. +- [Пример SerializeReferences](../../Samples~/SerializeReferences/Documentation/README.ru.md) — полиморфные данные оружия и эффектов в рабочей сцене. diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/05-profiler-markers.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/05-profiler-markers.md index b244a1f3..26c1eb9a 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/05-profiler-markers.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/05-profiler-markers.md @@ -1,59 +1,94 @@ # ProfilerMarkers -Регистрация `ProfilerMarker` через source generation. Генератор создаёт статический маркер для каждого места вызова, идентифицируемый по вызывающему методу и номеру строки. +`this.Marker()` создаёт маркер Unity Profiler с именем `Тип.Метод (строка)` — без статического поля `ProfilerMarker` и без имени, набранного руками. -```csharp -using UnityEngine; +## Быстрый старт + +Примеры на этой странице работают с классом `FlockSimulation` из [примера ProfilerMarkers](../../Samples~/ProfilerMarkers/Documentation/README.ru.md): + +| До — Unity API | После — FastTools | +|---|---| +|
private static readonly
    ProfilerMarker StepMarker =
    new("FlockSimulation.Step");

public void Step()
{
    using var _ = StepMarker.Auto();
    Integrate();
}
|
public void Step()
{
    using var _ = this.Marker();
    Integrate();
}
| + +Работает в `MonoBehaviour` и обычных C#-классах. Генератор входит в пакет; расширение находится в глобальном пространстве имён — дополнительные `using`, атрибуты и `partial` не нужны. + +## Marker() + +Возвращает `ProfilerMarker.AutoScope` маркера `Тип.Метод (строка)` для текущей точки вызова. -public class MyBehaviour : MonoBehaviour +> [!IMPORTANT] +> Не вызывайте `this.Marker()` без `using`: замер не завершится автоматически. Область не должна пересекать `await` или `yield`; измеряйте синхронные участки отдельно ([ограничение Unity](https://docs.unity3d.com/6000.0/Documentation/Manual/profiler-add-markers-code.html)). + +## WithName() + +`.WithName("Steering")` заменяет часть имени с методом; тип и номер строки остаются. + +```csharp +public void Step() { - private void DoSomething1() - { - using var _ = this.Marker(); - // Некоторый код - } + using var _ = this.Marker(); - private void DoSomething2() + using (this.Marker().WithName("Steering")) { - using (this.Marker()) + foreach (var agent in _agents) { - // Некоторый код - using var _ = this.Marker().WithName("Calculate"); - // Некоторый код + using var agentScope = this.Marker().WithName("Steering.Agent"); + ComputeSteering(agent); } } + + using (this.Marker().WithName("Integrate")) + { + Integrate(); + } } ``` -## Сгенерированный код +
+Сгенерированный код -```csharp -using Unity.Profiling; -using System.Runtime.CompilerServices; +Сокращённо: без `global::` и повторов атрибута. Номера строк считаются от начала блока выше; в реальном файле это строки исходника. -internal static class __MyBehaviourProfilerMarkerExtensions +```csharp +// +[GeneratedCode("Aspid.FastTools.Generators.ProfilerMarkersGenerator", "1.0.0")] +internal static class __FlockSimulationProfilerMarkerExtensions { - private static readonly ProfilerMarker DoSomething1_Marker_Line_7 = new("MyBehaviour.DoSomething1 (7)"); - private static readonly ProfilerMarker DoSomething2_Marker_Line_13 = new("MyBehaviour.DoSomething2 (13)"); - private static readonly ProfilerMarker DoSomething2_Marker_Line_16 = new("MyBehaviour.Calculate (16)"); + private static readonly ProfilerMarker Step = new("FlockSimulation.Step (3)"); + private static readonly ProfilerMarker Step_2 = new("FlockSimulation.Steering (5)"); + private static readonly ProfilerMarker Step_3 = new("FlockSimulation.Steering.Agent (9)"); + private static readonly ProfilerMarker Step_4 = new("FlockSimulation.Integrate (14)"); - public static ProfilerMarker.AutoScope Marker(this MyBehaviour _, [CallerLineNumberAttribute] int line = -1) + public static ProfilerMarker.AutoScope Marker(this FlockSimulation _, [CallerLineNumber] int line = -1) { #if ENABLE_PROFILER - if (line is 7) return DoSomething1_Marker_Line_7.Auto(); - if (line is 13) return DoSomething2_Marker_Line_13.Auto(); - if (line is 16) return DoSomething2_Marker_Line_16.Auto(); + if (line is 3) return Step.Auto(); + if (line is 5) return Step_2.Auto(); + if (line is 9) return Step_3.Auto(); + if (line is 14) return Step_4.Auto(); #endif return default; } } ``` -Тело диспетчера обёрнуто в `#if ENABLE_PROFILER`: в сборке без профайлера каждый вызов возвращает `default` и ничего не стоит. +
+ +Дерево в **CPU Usage → Hierarchy** повторяет вложенность `using`, а маркер внутри цикла остаётся одной строкой со счётчиком `Calls`. Deep Profile не нужен. + +![Схема маркеров FlockSimulation: Steering и Integrate вложены в Step, у Steering.Agent — 120 вызовов. Время приведено для примера.](../Images/profiler-markers-hierarchy.svg) + +Схема маркеров FlockSimulation: Steering и Integrate вложены в Step, у Steering.Agent — 120 вызовов. Время приведено для примера. + +`WithName` принимает только строковый литерал: `"Steering"`, `@"Steering"` или `$"Steering"` без подстановок. Переменные, `const`, `nameof`, конкатенация и `$"Agent {index}"` оставляют исходное имя метода — генератор читает текст исходника и не вычисляет выражения. Сам аргумент во время выполнения всё равно вычисляется. + +## Особенности генерации -- **Имя маркера** — `"{TypeName}.{method} ({line})"`; `.WithName("…")` заменяет часть с именем члена. Для generic-типов имя строится через `typeof(T).Name`, так что у каждого закрытого типа свой маркер. -- **Вызовы внутри лямбд и локальных функций** относятся к ближайшему объявленному методу, полю или свойству. +- **Номер строки.** Каждому вызову в типе нужна своя строка, в том числе в разных файлах `partial`. Перенос вызова меняет суффикс имени. +- **Имя члена.** Метод даёт своё имя, конструктор — `Ctor`, аксессор свойства — имя свойства. Лямбды и локальные функции используют член, в котором объявлены. +- **Generic-типы** получают отдельные маркеры на каждый закрытый тип: `Worker.Run()` → `Worker.Run (строка)`. +- **Без `ENABLE_PROFILER`** ничего не измеряется, но код внутри `using` и аргументы `WithName` по-прежнему выполняются. -## Результат +## Пример в пакете -![Сгенерированные маркеры в окне Unity Profiler](../Images/aspid_fasttools_profiler_markers.png) +Сцена со стаей из 120 агентов, где `FlockSimulation` размечен показанными выше маркерами: [ProfilerMarkers](../../Samples~/ProfilerMarkers/Documentation/README.ru.md). diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/06-enum-values.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/06-enum-values.md index 8cbf5dce..7eb3aec5 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/06-enum-values.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/06-enum-values.md @@ -1,53 +1,199 @@ # EnumValues -Сериализуемые отображения enum → значение, настраиваемые через Inspector. +Таблица значений по ключам enum, настраиваемая в инспекторе: множители урона, цвета, звуки, ссылки на ассеты. `GetValue` возвращает значение подходящей строки, а если её нет — `Default Value`. -## EnumValues\ +## Быстрый старт -Сериализуемая коллекция записей `EnumValue` с настраиваемым значением по умолчанию. Реализует `IEnumerable>`. +В примерах используется перечисление: -`GetValue` возвращает сопоставленное значение, а при отсутствии ключа — настроенное значение по умолчанию. `[Flags]`-перечисления поддерживаются: сопоставление использует `HasFlag` и корректно обрабатывает члены со значением `0`. +```csharp +public enum DamageType +{ + Physical, Fire, Ice, Poison +} +``` + +Добавьте `using Aspid.FastTools.Enums;` к скрипту с `using UnityEngine;`. Одна таблица заменяет набор сериализованных полей и `switch`: + +| До — отдельные поля и switch | После — EnumValues | +|---|---| +|
[SerializeField]
private float _defaultMultiplier = 1f;
[SerializeField]
private float _fireMultiplier = 1.5f;

public float GetMultiplier(
    DamageType type) => type switch
{
    DamageType.Fire => _fireMultiplier,
    _ => _defaultMultiplier
};
|
[SerializeField]
private EnumValues<DamageType, float>
    _multipliers;

public float GetMultiplier(
    DamageType type) =>
    _multipliers.GetValue(type);
| + +![Fire использует множитель 1.5, остальные типы урона — Default Value 1](../Images/enum-values-multipliers-quick-start.png) + +Fire использует множитель 1.5, остальные типы урона — Default Value 1 + +| Вызов | Результат | +|---|---| +| `_multipliers.GetValue(DamageType.Fire)` | `1.5` — значение строки `Fire` | +| `_multipliers.GetValue(DamageType.Ice)` | `1` — строки `Ice` нет, возвращается `Default Value` | + +## Настройка в инспекторе + +1. Раскройте таблицу и задайте **Default Value** — его получат ключи без собственной строки. +2. Добавьте строки вручную или нажмите правой кнопкой по свойству и выберите **Populate Missing Enum Members**. +3. Настройте значения добавленных строк. + +Строки нужны только ключам, чьё значение отличается от `Default Value`. + +### Populate Missing Enum Members + +Добавляет в конец таблицы недостающие члены enum со значением, равным текущему `Default Value`. + +![Populate Missing Enum Members добавляет строки со значением 1, сохраняя Fire = 1.5; Undo отменяет заполнение](../Images/enum-values-multipliers-populate.gif) + +Populate Missing Enum Members добавляет строки со значением 1, сохраняя Fire = 1.5; Undo отменяет заполнение + +Для `[Flags]` добавляются только объявленные члены, включая именованные комбинации. Все возможные сочетания битов не создаются. + +## Какой вариант выбрать + +| Задача | Тип поля | Выбор enum в инспекторе | Ключ в `GetValue` | +|---|---|---|---| +| Перечисление известно в коде | `EnumValues` | Задан аргументом `TEnum`; поле типа только для чтения | `TEnum`: проверяется компилятором, без упаковки в `object` | +| Перечисление выбирает автор ассета | `EnumValues` | Доступен в заголовке таблицы | `System.Enum`: ключ упаковывается, а чужой enum проходит компиляцию и возвращает `Default Value` | + +Оба варианта поддерживают `Default Value`, `[Flags]` и перебор строк. `TValue` — любой тип, который сериализует Unity: `float`, `Color`, `AudioClip`, ваш `[Serializable]`-класс. + +### EnumValues\ ```csharp -using System; -using UnityEngine; -using Aspid.FastTools.Enums; +[SerializeField] private EnumValues _multipliers; +``` -public enum DamageType { Physical, Fire, Ice, Poison } +Enum задан в коде, тип ключа проверяет компилятор. Полный пример — в [быстром старте](#быстрый-старт). -[Flags] -public enum StatusEffect { None = 0, Burning = 1, Frozen = 2, Slowed = 4, Stunned = 8 } +### EnumValues\ -public sealed class DamageDealer : MonoBehaviour -{ - [SerializeField] private EnumValues _damageMultipliers; +То же поле без `DamageType` в объявлении; enum выбирается в инспекторе: + +```csharp +[SerializeField] private EnumValues _multipliers; + +public float GetMultiplier(DamageType type) => _multipliers.GetValue(type); +``` + +Для этого примера выберите **DamageType** в заголовке таблицы. Ключ другого перечисления вернёт `Default Value`, даже если числовое значение совпало. + +![Откройте выбор типа в заголовке Multipliers и найдите DamageType](../Images/enum-values-type-selector.png) + +Откройте выбор типа в заголовке Multipliers и найдите DamageType + +> [!IMPORTANT] +> Если enum не выбран, таблица возвращает `Default Value` и при первом обращении пишет предупреждение в Console. Если сохранённый тип не найден в проекте, например после переименования, вместо предупреждения будет ошибка. + +## Правила поиска + +Таблица просматривается последовательно, сверху вниз. Для обычного enum побеждает первая строка с тем же числовым значением ключа, иначе — `Default Value`: - // Комбинации флагов (например Burning | Slowed) сопоставляются через HasFlag, побеждает первое - // совпадение — поэтому составные записи ставьте ПЕРЕД их отдельными флагами. - [SerializeField] private EnumValues _speedMultipliersByStatus; +| Ситуация | Результат | +|---|---| +| Ключ найден | Значение строки, в том числе `0`, `false` или `null` | +| Ключа нет или таблица пустая | `Default Value` | +| Несколько строк с одним числовым ключом | Первая из них | +| Разные имена enum с одним числовым значением | Для поиска это один ключ | - public float GetMultiplier(DamageType type) => _damageMultipliers.GetValue(type); +### Флаги - public float GetSpeedModifier(StatusEffect effects) => _speedMultipliersByStatus.GetValue(effects); +Для `[Flags]` сначала ищется точное совпадение, затем проверяется вхождение флагов. + +Значение `0` совпадает только с `0` и не подходит к остальным флагам как «пустая маска». Пример: + +```csharp +[Flags] +public enum StatusEffect +{ + None = 0, + Burning = 1, + Slowed = 2, + Frozen = 4 } + +[SerializeField] private EnumValues _speedMultipliers; ``` -![EnumValues в Инспекторе](../Images/aspid_fasttools_enum_values.png) +`Default Value` равен `1`, строки идут в таком порядке: + +| Ключ | Значение | +|---|---| +| `Burning` | `0.9` | +| `Slowed` | `0.5` | +| `Burning \| Slowed` | `0.3` | +| `None` | `1` | + +
    +
  1. + Точное совпадение + Ищем весь запрошенный набор флагов. + Burning | Slowed → 0.3 + Точная строка побеждает, даже если стоит ниже. + Нет точной строки → +
  2. +
  3. + Первая подходящая строка + Все её флаги должны входить в запрос. + Burning | Frozen → 0.9 + Выбирается Burning; порядок строк важен. + Нет подходящей строки → +
  4. +
  5. + Default Value + Возвращаем значение по умолчанию. + Frozen → 1 + Ни точной, ни подходящей строки нет. +
  6. +
+ +> [!NOTE] +> Второй проход берёт первую подходящую строку, а не самую полную. Для `Burning | Slowed | Frozen` подходят и `Burning`, и `Burning | Slowed`, но побеждает `Burning`, потому что стоит выше: результат `0.9`. Чтобы побеждала комбинация, ставьте составные строки выше одиночных флагов. + +## Проверка ключей через Equals + +`Equals(first, second)` сравнивает ключи по тем же правилам, не читая значения строк. Для обычного enum это равенство чисел. Для `[Flags]` метод проверяет, содержит ли **первый аргумент все биты второго**; ноль равен только нулю: -В Inspector выберите тип перечисления в заголовке `EnumValues`, затем назначьте значение для каждого члена перечисления. Нажмите правой кнопкой мыши по свойству, чтобы открыть контекстное меню с пунктом **Populate Missing Enum Members** — он добавит записи для всех отсутствующих членов перечисления, используя текущее Default Value как начальное значение. +```csharp +var combined = StatusEffect.Burning | StatusEffect.Slowed; -## EnumValues\ +_speedMultipliers.Equals(combined, StatusEffect.Burning); // true +_speedMultipliers.Equals(StatusEffect.Burning, combined); // false +_speedMultipliers.Equals(combined, StatusEffect.None); // false +_speedMultipliers.Equals(StatusEffect.None, StatusEffect.None); // true +``` + +Для строгого равенства значений enum используйте `==`. В `EnumValues` оба аргумента должны принадлежать выбранному перечислению, иначе результат — `false`. -Типизированный вариант `EnumValues` для частого случая, когда тип перечисления уже известен в коде. Тип фиксируется generic-аргументом, поэтому выбор типа в Inspector заблокирован, а обращения проверяются на этапе компиляции. Поиск не использует boxing — ключи сравниваются как закэшированные числовые значения, — а `foreach` по обоим вариантам использует struct-энумератор и не аллоцирует. Реализует `IEnumerable>`. +## Перебор строк + +`foreach` возвращает настроенные строки в порядке списка. `Default Value` и строки с нераспознанным ключом в перебор не входят: ```csharp -public sealed class HitEffect : MonoBehaviour +foreach (var (type, multiplier) in _multipliers) { - // Выбор типа в Inspector заблокирован — перечисление зафиксировано как DamageType. - [SerializeField] private EnumValues _damageColors; - - public Color GetColor(DamageType type) => _damageColors.GetValue(type); + Debug.Log($"{type}: {multiplier}"); } ``` -Семантика поиска (включая обработку `[Flags]`) идентична `EnumValues`. +Типизированная таблица выдаёт ключи `TEnum`, универсальная — `System.Enum`. Прямой `foreach` использует структурный перечислитель и не выделяет память; перебор через интерфейс `IEnumerable`, например в LINQ, упаковывает его. + +## Изменение таблицы и enum + +Публичный API только читает таблицу: методов `Add`, `Remove` и индексатора для записи нет, значения задаются через сериализацию Unity. + +Ключи хранятся по **именам** членов enum: + +| Изменение enum | Результат | +|---|---| +| Члены переставлены или изменены их числовые значения | Таблица работает как раньше | +| Добавлен член | Возвращает `Default Value`, пока не добавлена строка; **Populate Missing Enum Members** заполняет пропуск | +| Член переименован или удалён | Его строка не распознаётся: при инициализации в Console появляется ошибка, поиск и перебор её пропускают | + +> [!WARNING] +> Как только такая строка отрисовывается в инспекторе, её ключ молча заменяется на первый член enum. Переименовывайте члены до открытия ассета в инспекторе или проверьте строки сразу после. + +## Пример в пакете + +Плитки и следы получают цвет из `EnumValues`, а множитель скорости — из `EnumValues` с выбранным в инспекторе `[Flags]`-enum: [EnumValues](../../Samples~/EnumValues/Documentation/README.ru.md). + +![Персонаж проходит по разным поверхностям и оставляет непрерывную цветную линию.](../../Samples~/EnumValues/Documentation/Images/demo.gif) + +Персонаж проходит по разным поверхностям и оставляет непрерывную цветную линию. diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/07-visual-element-extensions.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/07-visual-element-extensions.md index c1f4e9f7..4063c57c 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/07-visual-element-extensions.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/07-visual-element-extensions.md @@ -1,648 +1,743 @@ # VisualElement Extensions - -Fluent-методы расширения для построения UIToolkit-деревьев в коде. Все методы возвращают `T` (сам элемент) для цепочки вызовов. - -```csharp -using Aspid.FastTools.UIElements; // runtime-расширения -using Aspid.FastTools.UIElements.Editors; // editor-only расширения (например, AddOpenScriptCommand) -``` - -## Пример - -Реактивный редактор для `ScriptableObject` `AbilityConfig` — заголовок и статус-пилла в шапке и Warning `HelpBox`, который переключается в зависимости от `ManaCost`. +Расширения UI Toolkit для построения деревьев элементов, настройки стилей, подписки на события и привязки полей в редакторе. Методы возвращают настраиваемый элемент, чтобы объединять вызовы в цепочки. + +## Быстрый старт + +Добавьте `using Aspid.FastTools.UIElements;` к скрипту с `using UnityEngine.UIElements;`. Так выглядит одна и та же панель с заголовком: + +| До — Unity API | После — FastTools | +|---|---| +|
var title = new Label("Stats");
title.style.fontSize = 18;

var panel = new VisualElement();
panel.style.paddingLeft = 12;
panel.style.paddingRight = 12;
panel.style.paddingTop = 8;
panel.style.paddingBottom = 8;
panel.Add(title);
|
var panel = new VisualElement()
    .SetPaddingX(12)
    .SetPaddingY(8)
    .AddChild(new Label("Stats")
        .SetFontSize(18));
| + +Сеттеры сохраняют тип: `new Button().SetText("Refresh")` возвращает `Button`. Операции с дочерними узлами возвращают **родителя** — следующий вызов продолжает настраивать его. + +## Найти нужное расширение + +| Задача | Раздел | +|---|---| +| Построить дерево, задать имя или доступность | [Элементы и дочерние узлы](#элементы-и-дочерние-узлы) | +| Управлять фокусом и клавиатурной навигацией | [Фокус](#фокус) | +| Подключить USS и переключить классы | [USS и классы](#uss-и-классы) | +| Задать размеры, отступы, цвет и рамку | [Стили](#стили) | +| Установить значение поля и подписаться на изменение | [Значения и события](#значения-и-события) | +| Настроить кнопку, поле или изображение | [Конкретные элементы](#конкретные-элементы) | +| Создать список с переиспользованием строк | [Списки и деревья](#списки-и-деревья) | +| Привязать SerializedObject или открыть скрипт | [Расширения редактора](#расширения-редактора) | +| Прочитать собственное свойство USS как enum | [Собственные свойства USS](#собственные-свойства-uss) | + +## Элементы и дочерние узлы + +| До — Unity API | После — FastTools | +|---|---| +|
element.name = name;
|
element.SetName(name);
| +|
element.visible = visible;
|
element.SetVisible(visible);
| +|
element.tooltip = tooltip;
|
element.SetTooltip(tooltip);
| +|
element.userData = data;
|
element.SetUserData(data);
| +|
element.SetEnabled(enabled);
|
element.SetEnabledSelf(enabled);
| +|
element.pickingMode = mode;
|
element.SetPickingMode(mode);
| +|
element.usageHints = hints;
|
element.SetUsageHints(hints);
| +|
element.viewDataKey = key;
|
element.SetViewDataKey(key);
| +|
element.languageDirection = direction;
|
element.SetLanguageDirection(direction);
| +|
element.disablePlayModeTint = disable;
|
element.SetDisablePlayModeTint(disable);
| +|
element.dataSource = source;
|
element.SetDataSource(source);
| +|
element.dataSourceType = type;
|
element.SetDataSourceType(type);
| +|
element.dataSourcePath = path;
|
element.SetDataSourcePath(path);
| + +| До — Unity API | После — FastTools | +|---|---| +|
panel.Add(child);
|
panel.AddChild(child);
panel.AddChildIf(condition, child);
| +|
foreach (var child in children)
    panel.Add(child);
|
panel.AddChildren(a, b, c);
panel.AddChildren(enumerable);
panel.AddChildren(list);
panel.AddChildren(span);
panel.AddChildren(readOnlySpan);
panel.AddChildrenIf(condition, …);
| +|
panel.Insert(index, child);
|
panel.InsertChild(index, child);
panel.InsertChildIf(condition, index, child);
| +|
foreach (var child in children)
    panel.Insert(index++, child);
|
panel.InsertChildren(index, a, b, c);
panel.InsertChildren(index, enumerable);
panel.InsertChildren(index, list);
panel.InsertChildren(index, span);
panel.InsertChildren(index, readOnlySpan);
panel.InsertChildrenIf(condition, index, …);
| +|
panel.Remove(child);
|
panel.RemoveChild(child);
panel.RemoveChildIf(condition, child);
| +|
panel.RemoveAt(index);
|
panel.RemoveChildAt(index);
panel.RemoveChildAtIf(condition, index);
| +|
panel.Clear();
|
panel.ClearChildren();
panel.ClearChildrenIf(condition);
| + +Эти методы возвращают родительский элемент, поэтому их можно объединять в цепочку. `AddChildren` и `InsertChildren` сохраняют порядок переданных элементов. + +> [!NOTE] +> `*If` проверяет условие только в момент вызова. Аргументы вычисляются заранее: `AddChildIf(false, new Label("Warning"))` создаст `Label`, но не добавит его в дерево. Для дорогого создания используйте обычный `if`. + +### Видимость и доступность + +| До — Unity API | После — FastTools | +|---|---| +|
element.visible = false;
|
element.SetVisible(false);
| +|
element.style.display =
    DisplayStyle.None;
|
element.SetDisplay(DisplayStyle.None);
| +|
element.SetEnabled(false);
|
element.SetEnabledSelf(false);
| + +## Фокус + +| До — Unity API | После — FastTools | +|---|---| +|
search.Focus();
|
search.FocusSelf();
| +|
search.Blur();
|
search.BlurSelf();
| +|
bool focused =
    search.focusController?.focusedElement
        == search;
|
bool focused = search.IsFocused();
| +|
search.tabIndex = 0;
|
search.SetTabIndex(0);
| +|
search.focusable = true;
|
search.SetFocusable(true);
| +|
search.delegatesFocus = true;
|
search.SetDelegatesFocus(true);
| + +## USS и классы + +| До — Unity API | После — FastTools | +|---|---| +|
panel.AddToClassList("ability-card");
|
panel.AddClass("ability-card");
| +|
panel.RemoveFromClassList("ability-card");
|
panel.RemoveClass("ability-card");
| +|
panel.ClearClassList();
|
panel.ClearClasses();
| +|
panel.ToggleInClassList("playing");
|
panel.ToggleClass("playing");
| +|
panel.EnableInClassList(
    "playing", Application.isPlaying);
|
panel.EnableClass(
    "playing", Application.isPlaying);
| +|
panel.styleSheets.Add(styleSheet);
|
panel.AddStyleSheet(styleSheet);
| +|
panel.styleSheets.Remove(styleSheet);
|
panel.RemoveStyleSheet(styleSheet);
| +|
panel.styleSheets.Add(
    Resources.Load<StyleSheet>("UI/AbilityCard"));
|
panel.AddStyleSheetFromResources("UI/AbilityCard");
| +|
panel.styleSheets.Remove(
    Resources.Load<StyleSheet>("UI/AbilityCard"));
|
panel.RemoveStyleSheetFromResources("UI/AbilityCard");
| + +## Стили + +### Стороны, оси и единицы измерения + +Общее значение задаёт все стороны; `X` — левую и правую, `Y` — верхнюю и нижнюю. В перегрузках с необязательными параметрами пропущенные стороны сохраняют прежнее значение: ```csharp -[CustomEditor(typeof(AbilityConfig))] -internal sealed class AbilityConfigEditor : Editor -{ - public override VisualElement CreateInspectorGUI() - { - var config = (AbilityConfig)target; - - var badge = new Label() - .SetFontSize(10).SetUnityFontStyleAndWeight(FontStyle.Bold) - .SetPaddingX(10).SetPaddingY(3).SetBorderRadius(10).SetBorderWidth(1); - - var helpBox = new HelpBox("This ability costs no mana — is that intentional?", HelpBoxMessageType.Warning) - .SetMarginTop(8).SetBorderRadius(6); - - Refresh(); - return new VisualElement() - .SetBorderRadius(10).SetBorderWidth(1).SetPaddingX(14).SetPaddingY(12) - .AddChild(new VisualElement() - .SetFlexDirection(FlexDirection.Row).SetAlignItems(Align.Center) - .AddChild(new Label(target.GetScriptName()).SetFlexGrow(1).SetFontSize(15)) - .AddChild(badge)) - .AddChild(new PropertyField(serializedObject.FindProperty("_manaCost")).AddValueChanged(_ => Refresh())) - .AddChild(helpBox); - - void Refresh() - { - var isFree = config.ManaCost is 0; - badge.SetText(isFree ? "FREE" : $"{config.ManaCost} MP"); - helpBox.SetDisplay(isFree ? DisplayStyle.Flex : DisplayStyle.None); - } - } -} +panel + .SetPadding(8) // все стороны + .SetPaddingX(12) // слева и справа + .SetMargin(top: 4, bottom: 8) + .SetSize(width: Length.Percent(100)); ``` -![Инспектор AbilityConfig, собранный fluent-расширениями](../Images/aspid_fasttools_visual_element.gif) +### Настройка через IStyle + +Те же методы доступны на `element.style`. Такая цепочка возвращает `IStyle`, поэтому продолжать её методами элемента нужно отдельным вызовом: + +| До — Unity API | После — FastTools | +|---|---| +|
panel.style.paddingLeft = 12;
panel.style.paddingRight = 12;
panel.style.height = 48;
|
panel.style
    .SetPaddingX(12)
    .SetHeight(48);
| + +### Справочник стилей + +Основные примеры рассчитаны на Unity 6.0. В раскрывающихся блоках отмечены методы для более новых версий Unity. + +
+Раскладка + +| До — Unity API | После — FastTools | +|---|---| +|
panel.style.flexBasis = 120;
|
panel.SetFlexBasis(120);
| +|
panel.style.flexGrow = 1;
|
panel.SetFlexGrow(1);
| +|
panel.style.flexShrink = 0;
|
panel.SetFlexShrink(0);
| +|
panel.style.flexWrap = Wrap.Wrap;
|
panel.SetFlexWrap(Wrap.Wrap);
| +|
panel.style.flexDirection = FlexDirection.Row;
|
panel.SetFlexDirection(FlexDirection.Row);
| +|
panel.style.alignSelf = Align.Center;
|
panel.SetAlignSelf(Align.Center);
| +|
panel.style.alignItems = Align.Center;
|
panel.SetAlignItems(Align.Center);
| +|
panel.style.alignContent = Align.Stretch;
|
panel.SetAlignContent(Align.Stretch);
| +|
panel.style.justifyContent = Justify.SpaceBetween;
|
panel.SetJustifyContent(Justify.SpaceBetween);
| +|
panel.style.position = Position.Absolute;
|
panel.SetPosition(Position.Absolute);
| + +
+ +
+Размеры + +| До — Unity API | После — FastTools | +|---|---| +|
panel.style.width = 48;
panel.style.height = 48;
|
panel.SetSize(48);
| +|
panel.style.width = 240;
panel.style.height = 48;
|
panel.SetSize(240, 48);
| +|
panel.style.width = Length.Percent(100);
|
panel.SetSize(width: Length.Percent(100));
| +|
panel.style.minWidth = 120;
panel.style.minHeight = 120;
|
panel.SetMinSize(120);
| +|
panel.style.minWidth = 120;
panel.style.minHeight = 32;
|
panel.SetMinSize(120, 32);
| +|
panel.style.minHeight = 32;
|
panel.SetMinSize(minHeight: 32);
| +|
panel.style.maxWidth = 480;
panel.style.maxHeight = 480;
|
panel.SetMaxSize(480);
| +|
panel.style.maxWidth = 480;
panel.style.maxHeight = 320;
|
panel.SetMaxSize(480, 320);
| +|
panel.style.maxWidth = 480;
|
panel.SetMaxSize(maxWidth: 480);
| +|
panel.style.width = 240;
|
panel.SetWidth(240);
| +|
panel.style.minWidth = 120;
|
panel.SetMinWidth(120);
| +|
panel.style.maxWidth = 480;
|
panel.SetMaxWidth(480);
| +|
panel.style.height = 48;
|
panel.SetHeight(48);
| +|
panel.style.minHeight = 32;
|
panel.SetMinHeight(32);
| +|
panel.style.maxHeight = 320;
|
panel.SetMaxHeight(320);
| + +
+ +
+Отступы и позиционирование + +| До — Unity API | После — FastTools | +|---|---| +|
panel.style.marginTop = 8;
panel.style.marginRight = 8;
panel.style.marginBottom = 8;
panel.style.marginLeft = 8;
|
panel.SetMargin(8);
| +|
panel.style.marginTop = 8;
panel.style.marginBottom = 8;
|
panel.SetMargin(top: 8, bottom: 8);
| +|
panel.style.marginLeft = 8;
panel.style.marginRight = 8;
|
panel.SetMarginX(8);
| +|
panel.style.marginTop = 8;
panel.style.marginBottom = 8;
|
panel.SetMarginY(8);
| +|
panel.style.marginTop = 8;
|
panel.SetMarginTop(8);
| +|
panel.style.marginRight = 8;
|
panel.SetMarginRight(8);
| +|
panel.style.marginBottom = 8;
|
panel.SetMarginBottom(8);
| +|
panel.style.marginLeft = 8;
|
panel.SetMarginLeft(8);
| +|
panel.style.paddingTop = 12;
panel.style.paddingRight = 12;
panel.style.paddingBottom = 12;
panel.style.paddingLeft = 12;
|
panel.SetPadding(12);
| +|
panel.style.paddingTop = 12;
panel.style.paddingBottom = 12;
|
panel.SetPadding(top: 12, bottom: 12);
| +|
panel.style.paddingLeft = 12;
panel.style.paddingRight = 12;
|
panel.SetPaddingX(12);
| +|
panel.style.paddingTop = 12;
panel.style.paddingBottom = 12;
|
panel.SetPaddingY(12);
| +|
panel.style.paddingTop = 12;
|
panel.SetPaddingTop(12);
| +|
panel.style.paddingRight = 12;
|
panel.SetPaddingRight(12);
| +|
panel.style.paddingBottom = 12;
|
panel.SetPaddingBottom(12);
| +|
panel.style.paddingLeft = 12;
|
panel.SetPaddingLeft(12);
| +|
panel.style.top = 0;
panel.style.right = 0;
panel.style.bottom = 0;
panel.style.left = 0;
|
panel.SetDistance(0);
| +|
panel.style.top = 0;
panel.style.bottom = 0;
|
panel.SetDistance(top: 0, bottom: 0);
| +|
panel.style.left = 0;
panel.style.right = 0;
|
panel.SetDistanceX(0);
| +|
panel.style.top = 0;
panel.style.bottom = 0;
|
panel.SetDistanceY(0);
| +|
panel.style.top = 0;
|
panel.SetTop(0);
| +|
panel.style.right = 0;
|
panel.SetRight(0);
| +|
panel.style.bottom = 0;
|
panel.SetBottom(0);
| +|
panel.style.left = 0;
|
panel.SetLeft(0);
| -## Core element operations - -```csharp -element - .SetName("MyElement") - .SetVisible(true) - .SetTooltip("Текст подсказки") - .AddChild(new Label("Hello")) - .AddChildren(child1, child2, child3); -``` +> `SetDistance` — обёртка для четырёх свойств `top`/`right`/`bottom`/`left`, используемых при абсолютном позиционировании. `SetTop`, `SetRight`, `SetBottom`, `SetLeft` — это прямые алиасы для одного свойства. -| Метод | Описание | -|-------|----------| -| `SetName(string)` | Устанавливает `element.name` | -| `SetVisible(bool)` | Устанавливает `element.visible` | -| `SetTooltip(string)` | Устанавливает `element.tooltip` | -| `SetUserData(object)` | Устанавливает `element.userData` | -| `SetEnabledSelf(bool)` | Устанавливает `element.enabledSelf` | -| `SetPickingMode(PickingMode)` | Устанавливает `element.pickingMode` | -| `SetUsageHints(UsageHints)` | Устанавливает `element.usageHints` | -| `SetViewDataKey(string)` | Устанавливает `element.viewDataKey` | -| `SetLanguageDirection(LanguageDirection)` | Устанавливает `element.languageDirection` | -| `SetDisablePlayModeTint(bool)` | Устанавливает `element.disablePlayModeTint` | -| `SetDataSource(object)` | Устанавливает `element.dataSource` | -| `SetDataSourceType(Type)` | Устанавливает `element.dataSourceType` | -| `SetDataSourcePath(PropertyPath)` | Устанавливает `element.dataSourcePath` | -| `AddChild(VisualElement)` | Добавляет дочерний элемент, возвращает родителя | -| `AddChildren(params VisualElement[])` | Добавляет несколько дочерних элементов | -| `AddChildren(IEnumerable)` | Добавляет из последовательности | -| `AddChildren(List)` | Добавляет из списка | -| `AddChildren(Span)` | Добавляет из span | -| `AddChildren(ReadOnlySpan)` | Добавляет из read-only span | -| `InsertChild(int, VisualElement)` | Вставляет дочерний элемент по указанному индексу | -| `InsertChildren(int, params VisualElement[])` | Вставляет несколько дочерних элементов начиная с индекса | -| `InsertChildren(int, IEnumerable)` | Вставляет из последовательности | -| `InsertChildren(int, List)` | Вставляет из списка | -| `InsertChildren(int, Span)` | Вставляет из span | -| `InsertChildren(int, ReadOnlySpan)` | Вставляет из read-only span | -| `RemoveChild(VisualElement)` | Удаляет дочерний элемент, возвращает родителя | -| `RemoveChildAt(int)` | Удаляет дочерний элемент по указанному индексу | -| `ClearChildren()` | Удаляет все дочерние элементы | - -> У каждой операции с дочерними элементами есть `*If`-вариант (`AddChildIf`, `AddChildrenIf`, `InsertChildIf`, `InsertChildrenIf`, `RemoveChildIf`, `RemoveChildAtIf`, `ClearChildrenIf`) с ведущим параметром `bool condition` — операция выполняется только при `condition == true`. - -> `RegisterCallbackOnce` и `RegisterCallbackOnce` доступны на всех версиях Unity (пакет содержит polyfill для версий до 2023.1). - -## Focusable - -| Метод | Описание | -|-------|----------| -| `FocusSelf()` | Устанавливает фокус на элемент | -| `BlurSelf()` | Снимает фокус с элемента | -| `IsFocused()` | Возвращает, находится ли элемент в фокусе | -| `SetTabIndex(int)` | Устанавливает `element.tabIndex` | -| `SetFocusable(bool)` | Устанавливает `element.focusable` | -| `SetDelegatesFocus(bool)` | Устанавливает `element.delegatesFocus` | - -## USS & class operations - -| Метод | Описание | -|-------|----------| -| `AddClass(string)` | Добавляет USS-класс | -| `RemoveClass(string)` | Удаляет USS-класс | -| `ClearClasses()` | Удаляет все USS-классы | -| `ToggleClass(string)` | Переключает USS-класс вкл/выкл | -| `EnableClass(string, bool)` | Добавляет или удаляет USS-класс по условию | -| `AddStyleSheet(StyleSheet)` | Добавляет `StyleSheet` | -| `RemoveStyleSheet(StyleSheet)` | Удаляет `StyleSheet` | -| `AddStyleSheetFromResources(string)` | Добавляет таблицу стилей через `Resources.Load` | -| `RemoveStyleSheetFromResources(string)` | Удаляет таблицу стилей, загруженную через `Resources.Load` | - -## Style extensions — by category - -Все методы стилей также доступны напрямую на `IStyle` (те же имена методов, работают с объектом стиля). - -### Layout - -| Метод | Свойство стиля | -|-------|----------------| -| `SetFlexBasis(StyleLength)` | `flexBasis` | -| `SetFlexGrow(StyleFloat)` | `flexGrow` | -| `SetFlexShrink(StyleFloat)` | `flexShrink` | -| `SetFlexWrap(StyleEnum)` | `flexWrap` | -| `SetFlexDirection(FlexDirection)` | `flexDirection` | -| `SetAlignSelf(StyleEnum)` | `alignSelf` | -| `SetAlignItems(StyleEnum)` | `alignItems` | -| `SetAlignContent(StyleEnum)` | `alignContent` | -| `SetJustifyContent(StyleEnum)` | `justifyContent` | -| `SetPosition(StyleEnum)` | `position` | - -### Size - -| Метод | Описание | -|-------|----------| -| `SetSize(StyleLength)` | Устанавливает ширину и высоту одновременно | -| `SetSize(width?, height?)` | Устанавливает ширину и/или высоту независимо | -| `SetMinSize(StyleLength)` | Устанавливает minWidth и minHeight одновременно | -| `SetMinSize(width?, height?)` | | -| `SetMaxSize(StyleLength)` | Устанавливает maxWidth и maxHeight одновременно | -| `SetMaxSize(width?, height?)` | | -| `SetWidth(StyleLength)` | `width` | -| `SetMinWidth(StyleLength)` | `minWidth` | -| `SetMaxWidth(StyleLength)` | `maxWidth` | -| `SetHeight(StyleLength)` | `height` | -| `SetMinHeight(StyleLength)` | `minHeight` | -| `SetMaxHeight(StyleLength)` | `maxHeight` | - -### Spacing - -Все методы отступов имеют перегрузку с единым значением, перегрузку по сторонам (`top`, `right`, `bottom`, `left`), сеттеры по одной стороне и сеттеры по парам осей X/Y. - -| Метод | Свойства стиля | -|-------|----------------| -| `SetMargin(…)` / `SetPadding(…)` / `SetDistance(…)` | `Top/Right/Bottom/Left` (общее значение или per-side) | -| `SetMarginX/Y` · `SetPaddingX/Y` · `SetDistanceX/Y` | Устанавливает горизонтальную (X = `Left`+`Right`) или вертикальную (Y = `Top`+`Bottom`) пару | -| `SetMarginTop/Right/Bottom/Left` | Margin одной стороны | -| `SetPaddingTop/Right/Bottom/Left` | Padding одной стороны | -| `SetDistanceTop/Right/Bottom/Left` *(через `SetTop` / `SetRight` / `SetBottom` / `SetLeft`)* | Смещение одной стороны для абсолютного позиционирования (свойства `top` / `right` / `bottom` / `left`) | +
-> `SetDistance` — обёртка для четырёх свойств `top`/`right`/`bottom`/`left`, используемых при абсолютном позиционировании. `SetTop`, `SetRight`, `SetBottom`, `SetLeft` — это прямые алиасы для одного свойства. +
+Шрифт -### Font +| До — Unity API | После — FastTools | +|---|---| +|
panel.style.unityFont = font;
|
panel.SetUnityFont(font);
| +|
panel.style.fontSize = 14;
|
panel.SetFontSize(14);
| +|
panel.style.unityFontDefinition = fontDefinition;
|
panel.SetUnityFontDefinition(fontDefinition);
| +|
panel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
panel.SetUnityFontStyleAndWeight(FontStyle.Bold);
| -| Метод | Свойство стиля | -|-------|----------------| -| `SetUnityFont(StyleFont)` | `unityFont` | -| `SetFontSize(StyleLength)` | `fontSize` | -| `SetUnityFontDefinition(StyleFontDefinition)` | `unityFontDefinition` | -| `SetUnityFontStyleAndWeight(StyleEnum)` | `unityFontStyleAndWeight` | +
-### Font style presets +
+Начертание шрифта Удобные методы для переключения bold / italic без перезаписи другого флага: -| Метод | Описание | -|-------|----------| -| `SetNormalUnityFontStyleAndWeight()` | Сбрасывает в `FontStyle.Normal` | -| `AddBoldUnityFontStyleAndWeight()` | Добавляет bold, сохраняя italic | -| `RemoveBoldUnityFontStyleAndWeight()` | Убирает bold, сохраняя italic | -| `AddItalicUnityFontStyleAndWeight()` | Добавляет italic, сохраняя bold | -| `RemoveItalicUnityFontStyleAndWeight()` | Убирает italic, сохраняя bold | - -### Text - -| Метод | Свойство стиля | Примечания | -|-------|---------------|------------| -| `SetWordSpacing(StyleLength)` | `wordSpacing` | | -| `SetLetterSpacing(StyleLength)` | `letterSpacing` | | -| `SetUnityTextAlign(TextAnchor)` | `unityTextAlign` | | -| `SetTextShadow(StyleTextShadow)` | `textShadow` | | -| `SetUnityTextOutlineColor(StyleColor)` | `unityTextOutlineColor` | | -| `SetUnityTextOutlineWidth(StyleFloat)` | `unityTextOutlineWidth` | | -| `SetUnityParagraphSpacing(StyleLength)` | `unityParagraphSpacing` | | -| `SetTextOverflow(StyleEnum)` | `textOverflow` | | -| `SetUnityTextOverflowPosition(TextOverflowPosition)` | `unityTextOverflowPosition` | | -| `SetUnityTextGenerator(TextGeneratorType)` | `unityTextGenerator` | Unity 6+ | -| `SetUnityEditorTextRenderingMode(EditorTextRenderingMode)` | `unityEditorTextRenderingMode` | Unity 6+ | -| `SetUnityTextAutoSize(StyleTextAutoSize)` | `unityTextAutoSize` | Unity 6.2+ | -| `SetWhiteSpace(StyleEnum)` | `whiteSpace` | | - -### Color & Opacity - -| Метод | Свойство стиля | -|-------|----------------| -| `SetColor(StyleColor)` | `color` | -| `SetColor(string)` | `color`, разобранный из HTML-строки (`"#RRGGBB"` или именованный цвет) | -| `SetOpacity(StyleFloat)` | `opacity` | - -### Border - -| Метод | Описание | -|-------|----------| -| `SetBorderColor(StyleColor)` | Все стороны | -| `SetBorderColor(top?, right?, bottom?, left?)` | По стороне | -| `SetBorderColorX(StyleColor)` · `SetBorderColorY(StyleColor)` | Горизонтальная (left + right) или вертикальная (top + bottom) пара | -| `SetBorderColorTop/Right/Bottom/Left(StyleColor)` | Одна сторона | -| `SetBorderRadius(StyleLength)` | Все углы | -| `SetBorderRadius(topLeft?, topRight?, bottomLeft?, bottomRight?)` | По углу | -| `SetBorderRadiusTop(StyleLength)` · `SetBorderRadiusBottom(StyleLength)` | Пара верхних или нижних углов | -| `SetBorderRadiusTopLeft/TopRight/BottomLeft/BottomRight(StyleLength)` | Один угол | -| `SetBorderWidth(StyleFloat)` | Все стороны | -| `SetBorderWidth(top?, right?, bottom?, left?)` | По стороне | -| `SetBorderWidthX(StyleFloat)` · `SetBorderWidthY(StyleFloat)` | Горизонтальная или вертикальная пара | -| `SetBorderWidthTop/Right/Bottom/Left(StyleFloat)` | Одна сторона | - -### Background - -| Метод | Свойство стиля | -|-------|----------------| -| `SetBackgroundColor(StyleColor)` | `backgroundColor` | -| `SetBackgroundColor(string)` | `backgroundColor`, разобранный из HTML-строки (`"#RRGGBB"` или именованный цвет) | -| `SetBackgroundImage(StyleBackground)` | `backgroundImage` | -| `SetBackgroundImageFromResources(string)` | Загружает `Texture2D` через `Resources.Load` и присваивает его в `backgroundImage` | -| `SetBackgroundSize(StyleBackgroundSize)` | `backgroundSize` | -| `SetBackgroundRepeat(StyleBackgroundRepeat)` | `backgroundRepeat` | -| `SetBackgroundPosition(StyleBackgroundPosition)` | X и Y одновременно | -| `SetBackgroundPosition(x?, y?)` | Независимо | -| `SetBackgroundPositionX(StyleBackgroundPosition)` | `backgroundPositionX` | -| `SetBackgroundPositionY(StyleBackgroundPosition)` | `backgroundPositionY` | -| `SetUnityBackgroundImageTintColor(StyleColor)` | `unityBackgroundImageTintColor` | - -### Transform - -| Метод | Свойство стиля | -|-------|----------------| -| `SetScale(StyleScale)` | `scale` | -| `SetRotate(StyleRotate)` | `rotate` | -| `SetTranslate(StyleTranslate)` | `translate` | -| `SetTransformOrigin(StyleTransformOrigin)` | `transformOrigin` | - -### Aspect, Filter & Material +| До — Unity API | После — FastTools | +|---|---| +|
panel.style.unityFontStyleAndWeight =
    FontStyle.Normal;
|
panel.SetNormalUnityFontStyleAndWeight();
| +|
var current =
    panel.style.unityFontStyleAndWeight.value;
panel.style.unityFontStyleAndWeight =
    current == FontStyle.Italic
        ? FontStyle.BoldAndItalic
        : FontStyle.Bold;
|
panel.AddBoldUnityFontStyleAndWeight();
| +|
var current =
    panel.style.unityFontStyleAndWeight.value;
panel.style.unityFontStyleAndWeight =
    current == FontStyle.BoldAndItalic
        ? FontStyle.Italic
        : FontStyle.Normal;
|
panel.RemoveBoldUnityFontStyleAndWeight();
| +|
var current =
    panel.style.unityFontStyleAndWeight.value;
panel.style.unityFontStyleAndWeight =
    current == FontStyle.Bold
        ? FontStyle.BoldAndItalic
        : FontStyle.Italic;
|
panel.AddItalicUnityFontStyleAndWeight();
| +|
var current =
    panel.style.unityFontStyleAndWeight.value;
panel.style.unityFontStyleAndWeight =
    current == FontStyle.BoldAndItalic
        ? FontStyle.Bold
        : FontStyle.Normal;
|
panel.RemoveItalicUnityFontStyleAndWeight();
| + +
+ +
+Текст + +| До — Unity API | После — FastTools | +|---|---| +|
panel.style.wordSpacing = 2;
|
panel.SetWordSpacing(2);
| +|
panel.style.letterSpacing = 1;
|
panel.SetLetterSpacing(1);
| +|
panel.style.unityTextAlign = TextAnchor.MiddleCenter;
|
panel.SetUnityTextAlign(TextAnchor.MiddleCenter);
| +|
panel.style.textShadow = shadow;
|
panel.SetTextShadow(shadow);
| +|
panel.style.unityTextOutlineColor = Color.black;
|
panel.SetUnityTextOutlineColor(Color.black);
| +|
panel.style.unityTextOutlineWidth = 1;
|
panel.SetUnityTextOutlineWidth(1);
| +|
panel.style.unityParagraphSpacing = 8;
|
panel.SetUnityParagraphSpacing(8);
| +|
panel.style.textOverflow = TextOverflow.Ellipsis;
|
panel.SetTextOverflow(TextOverflow.Ellipsis);
| +|
panel.style.unityTextOverflowPosition = 
    TextOverflowPosition.End;
|
panel.SetUnityTextOverflowPosition(
    TextOverflowPosition.End);
| +|
panel.style.unityTextGenerator = 
    TextGeneratorType.Advanced;
|
panel.SetUnityTextGenerator(
    TextGeneratorType.Advanced);
| +|
panel.style.unityEditorTextRenderingMode = 
    EditorTextRenderingMode.SDF;
|
panel.SetUnityEditorTextRenderingMode(
    EditorTextRenderingMode.SDF);
| +|
panel.style.whiteSpace = WhiteSpace.NoWrap;
|
panel.SetWhiteSpace(WhiteSpace.NoWrap);
| +|
// Unity 6.2+
panel.style.unityTextAutoSize = autoSize;
|
// Unity 6.2+
panel.SetUnityTextAutoSize(autoSize);
| + +
+ +
+Цвет и прозрачность + +| До — Unity API | После — FastTools | +|---|---| +|
panel.style.color = Color.white;
|
panel.SetColor(Color.white);
| +|
if (ColorUtility.TryParseHtmlString(
        "#FF8800", out var color))
    panel.style.color = color;
|
panel.SetColor("#FF8800");
| +|
panel.style.opacity = 0.5f;
|
panel.SetOpacity(0.5f);
| + +
+ +
+Рамка + +| До — Unity API | После — FastTools | +|---|---| +|
panel.style.borderTopColor = Color.gray;
panel.style.borderRightColor = Color.gray;
panel.style.borderBottomColor = Color.gray;
panel.style.borderLeftColor = Color.gray;
|
panel.SetBorderColor(Color.gray);
| +|
if (ColorUtility.TryParseHtmlString(
        "#333333", out var color))
{
    panel.style.borderTopColor = color;
    panel.style.borderRightColor = color;
    panel.style.borderBottomColor = color;
    panel.style.borderLeftColor = color;
}
|
panel.SetBorderColor("#333333");
| +|
panel.style.borderTopColor = Color.gray;
panel.style.borderBottomColor = Color.gray;
|
panel.SetBorderColor(
    top: Color.gray, bottom: Color.gray);
| +|
panel.style.borderLeftColor = Color.gray;
panel.style.borderRightColor = Color.gray;
|
panel.SetBorderColorX(Color.gray);
| +|
panel.style.borderTopColor = Color.gray;
panel.style.borderBottomColor = Color.gray;
|
panel.SetBorderColorY(Color.gray);
| +|
panel.style.borderTopColor = Color.gray;
|
panel.SetBorderColorTop(Color.gray);
| +|
panel.style.borderRightColor = Color.gray;
|
panel.SetBorderColorRight(Color.gray);
| +|
panel.style.borderBottomColor = Color.gray;
|
panel.SetBorderColorBottom(Color.gray);
| +|
panel.style.borderLeftColor = Color.gray;
|
panel.SetBorderColorLeft(Color.gray);
| +|
panel.style.borderTopLeftRadius = 6;
panel.style.borderTopRightRadius = 6;
panel.style.borderBottomRightRadius = 6;
panel.style.borderBottomLeftRadius = 6;
|
panel.SetBorderRadius(6);
| +|
panel.style.borderTopLeftRadius = 6;
panel.style.borderTopRightRadius = 6;
|
panel.SetBorderRadius(
    topLeft: 6, topRight: 6);
| +|
panel.style.borderTopLeftRadius = 6;
panel.style.borderTopRightRadius = 6;
|
panel.SetBorderRadiusTop(6);
| +|
panel.style.borderBottomLeftRadius = 6;
panel.style.borderBottomRightRadius = 6;
|
panel.SetBorderRadiusBottom(6);
| +|
panel.style.borderTopLeftRadius = 6;
panel.style.borderBottomLeftRadius = 6;
|
panel.SetBorderRadiusLeft(6);
| +|
panel.style.borderTopRightRadius = 6;
panel.style.borderBottomRightRadius = 6;
|
panel.SetBorderRadiusRight(6);
| +|
panel.style.borderTopLeftRadius = 6;
|
panel.SetBorderRadiusTopLeft(6);
| +|
panel.style.borderTopRightRadius = 6;
|
panel.SetBorderRadiusTopRight(6);
| +|
panel.style.borderBottomRightRadius = 6;
|
panel.SetBorderRadiusBottomRight(6);
| +|
panel.style.borderBottomLeftRadius = 6;
|
panel.SetBorderRadiusBottomLeft(6);
| +|
panel.style.borderTopWidth = 1;
panel.style.borderRightWidth = 1;
panel.style.borderBottomWidth = 1;
panel.style.borderLeftWidth = 1;
|
panel.SetBorderWidth(1);
| +|
panel.style.borderTopWidth = 1;
panel.style.borderBottomWidth = 1;
|
panel.SetBorderWidth(top: 1, bottom: 1);
| +|
panel.style.borderLeftWidth = 1;
panel.style.borderRightWidth = 1;
|
panel.SetBorderWidthX(1);
| +|
panel.style.borderTopWidth = 1;
panel.style.borderBottomWidth = 1;
|
panel.SetBorderWidthY(1);
| +|
panel.style.borderTopWidth = 1;
|
panel.SetBorderWidthTop(1);
| +|
panel.style.borderRightWidth = 1;
|
panel.SetBorderWidthRight(1);
| +|
panel.style.borderBottomWidth = 1;
|
panel.SetBorderWidthBottom(1);
| +|
panel.style.borderLeftWidth = 1;
|
panel.SetBorderWidthLeft(1);
| + +
+ +
+Фон + +| До — Unity API | После — FastTools | +|---|---| +|
panel.style.backgroundColor = Color.black;
|
panel.SetBackgroundColor(Color.black);
| +|
if (ColorUtility.TryParseHtmlString(
        "#1B1B1B", out var color))
    panel.style.backgroundColor = color;
|
panel.SetBackgroundColor("#1B1B1B");
| +|
panel.style.backgroundImage = texture;
|
panel.SetBackgroundImage(texture);
| +|
panel.style.backgroundImage =
    Resources.Load<Texture2D>("UI/CardBackground");
|
panel.SetBackgroundImageFromResources(
    "UI/CardBackground");
| +|
panel.style.backgroundSize = backgroundSize;
|
panel.SetBackgroundSize(backgroundSize);
| +|
panel.style.backgroundRepeat = backgroundRepeat;
|
panel.SetBackgroundRepeat(backgroundRepeat);
| +|
panel.style.backgroundPositionX = position;
panel.style.backgroundPositionY = position;
|
panel.SetBackgroundPosition(position);
| +|
panel.style.backgroundPositionY = position;
|
panel.SetBackgroundPosition(y: position);
| +|
panel.style.backgroundPositionX = position;
|
panel.SetBackgroundPositionX(position);
| +|
panel.style.backgroundPositionY = position;
|
panel.SetBackgroundPositionY(position);
| +|
panel.style.unityBackgroundImageTintColor =
    Color.white;
|
panel.SetUnityBackgroundImageTintColor(
    Color.white);
| + +
+ +
+Трансформации + +| До — Unity API | После — FastTools | +|---|---| +|
panel.style.scale = new Scale(Vector2.one * 1.2f);
|
panel.SetScale(new Scale(Vector2.one * 1.2f));
| +|
panel.style.rotate = new Rotate(45);
|
panel.SetRotate(new Rotate(45));
| +|
panel.style.translate = new Translate(8, 0);
|
panel.SetTranslate(new Translate(8, 0));
| +|
panel.style.transformOrigin = transformOrigin;
|
panel.SetTransformOrigin(transformOrigin);
| + +
+ +
+Пропорции, фильтры и материал Доступно начиная с Unity 6000.3+. -| Метод | Свойство стиля | -|-------|----------------| -| `SetAspectRatio(StyleRatio)` | `aspectRatio` | -| `SetFilter(StyleList)` | `filter` | -| `SetUnityMaterial(StyleMaterialDefinition)` | `unityMaterial` | - -### Transition - -| Метод | Свойство стиля | -|-------|----------------| -| `SetTransitionDelay(StyleList)` | `transitionDelay` | -| `SetTransitionDuration(StyleList)` | `transitionDuration` | -| `SetTransitionProperty(StyleList)` | `transitionProperty` | -| `SetTransitionTimingFunction(StyleList)` | `transitionTimingFunction` | - -### Overflow & Visibility - -| Метод | Свойство стиля | -|-------|----------------| -| `SetOverflow(StyleEnum)` | `overflow` | -| `SetUnityOverflowClipBox(StyleEnum)` | `unityOverflowClipBox` | -| `SetVisibility(StyleEnum)` | `visibility` | -| `SetDisplay(DisplayStyle)` | `display` | +| До — Unity API | После — FastTools | +|---|---| +|
panel.style.aspectRatio = aspectRatio;
|
panel.SetAspectRatio(aspectRatio);
| +|
panel.style.filter = filter;
|
panel.SetFilter(filter);
| +|
panel.style.unityMaterial = material;
|
panel.SetUnityMaterial(material);
| -### Unity Slice +
-| Метод | Описание | -|-------|----------| -| `SetUnitySlice(StyleInt)` | Все стороны | -| `SetUnitySlice(top?, right?, bottom?, left?)` | По стороне | -| `SetUnitySliceX(StyleInt)` · `SetUnitySliceY(StyleInt)` | Горизонтальная (left + right) или вертикальная (top + bottom) пара | -| `SetUnitySliceTop/Right/Bottom/Left(StyleInt)` | Одна сторона | -| `SetUnitySliceScale(StyleFloat)` | `unitySliceScale` | -| `SetUnitySliceType(StyleEnum)` | Unity 6+ | +
+Переходы -### Cursor +| До — Unity API | После — FastTools | +|---|---| +|
panel.style.transitionDelay =
    new List<TimeValue> { 0.1f };
|
panel.SetTransitionDelay(
    new List<TimeValue> { 0.1f });
| +|
panel.style.transitionDuration =
    new List<TimeValue> { 0.3f };
|
panel.SetTransitionDuration(
    new List<TimeValue> { 0.3f });
| +|
panel.style.transitionProperty =
    new List<StylePropertyName> { "opacity" };
|
panel.SetTransitionProperty(
    new List<StylePropertyName> { "opacity" });
| +|
panel.style.transitionTimingFunction =
    new List<EasingFunction> { EasingMode.EaseInOut };
|
panel.SetTransitionTimingFunction(
    new List<EasingFunction> { EasingMode.EaseInOut });
| -| Метод | Свойство стиля | -|-------|----------------| -| `SetCursor(StyleCursor)` | `cursor` | +
-## Specialized element extensions +
+Переполнение и видимость -### TextElement +| До — Unity API | После — FastTools | +|---|---| +|
panel.style.overflow = Overflow.Hidden;
|
panel.SetOverflow(Overflow.Hidden);
| +|
panel.style.unityOverflowClipBox = 
    OverflowClipBox.ContentBox;
|
panel.SetUnityOverflowClipBox(
    OverflowClipBox.ContentBox);
| +|
panel.style.visibility = Visibility.Hidden;
|
panel.SetVisibility(Visibility.Hidden);
| +|
panel.style.display = DisplayStyle.None;
|
panel.SetDisplay(DisplayStyle.None);
| -```csharp -label - .SetText("Hello World") - .SetEnableRichText(true) - .SetParseEscapeSequences(true); -``` +
-| Метод | Описание | -|-------|----------| -| `SetText(string)` | Устанавливает отображаемый текст | -| `SetEnableRichText(bool)` | Включает разбор тегов rich-text | -| `SetEmojiFallbackSupport(bool)` | Включает emoji-fallback при рендеринге | -| `SetParseEscapeSequences(bool)` | Обрабатывать ли escape-последовательности (например, `\n`) | -| `SetDisplayTooltipWhenElided(bool)` | Показывать обрезанный текст в подсказке при наведении | +
+Нарезка изображения -### ITextEdition (TextField, IntegerField, …) +| До — Unity API | После — FastTools | +|---|---| +|
panel.style.unitySliceTop = 4;
panel.style.unitySliceRight = 4;
panel.style.unitySliceBottom = 4;
panel.style.unitySliceLeft = 4;
|
panel.SetUnitySlice(4);
| +|
panel.style.unitySliceTop = 4;
panel.style.unitySliceBottom = 4;
|
panel.SetUnitySlice(top: 4, bottom: 4);
| +|
panel.style.unitySliceLeft = 4;
panel.style.unitySliceRight = 4;
|
panel.SetUnitySliceX(4);
| +|
panel.style.unitySliceTop = 4;
panel.style.unitySliceBottom = 4;
|
panel.SetUnitySliceY(4);
| +|
panel.style.unitySliceTop = 4;
|
panel.SetUnitySliceTop(4);
| +|
panel.style.unitySliceRight = 4;
|
panel.SetUnitySliceRight(4);
| +|
panel.style.unitySliceBottom = 4;
|
panel.SetUnitySliceBottom(4);
| +|
panel.style.unitySliceLeft = 4;
|
panel.SetUnitySliceLeft(4);
| +|
panel.style.unitySliceScale = 1;
|
panel.SetUnitySliceScale(1);
| +|
panel.style.unitySliceType = SliceType.Sliced;
|
panel.SetUnitySliceType(SliceType.Sliced);
| -```csharp -textField - .SetPlaceholder("Поиск…") - .SetMaxLength(64) - .SetDelayed(true); -``` +
-| Метод | Описание | -|-------|----------| -| `SetMaxLength(int)` | Максимальное число символов | -| `SetMaskChar(char)` | Символ для маскировки пароля | -| `SetDelayed(bool)` | Откладывает изменение значения до потери фокуса / Enter | -| `SetReadOnly(bool)` | Запрещает редактирование | -| `SetPassword(bool)` | Включает password-режим (использует mask char) | -| `SetPlaceholder(string)` | Текст-плейсхолдер для пустого поля | -| `SetAutoCorrection(bool)` | Включает автокоррекцию (mobile) | -| `SetHideMobileInput(bool)` | Скрывает мобильный soft input | -| `SetHideSoftKeyboard(bool)` | Скрывает экранную клавиатуру | -| `SetHidePlaceholderOnFocus(bool)` | Убирает плейсхолдер при фокусе | -| `SetKeyboardType(TouchScreenKeyboardType)` | Тип touch-screen клавиатуры | - -### ITextSelection +
+Курсор -```csharp -textField - .SetSelectable(true) - .SetSelectAllOnFocus(true) - .AddOnCursorIndexChange(() => Debug.Log(textField.cursorIndex)); -``` +| До — Unity API | После — FastTools | +|---|---| +|
panel.style.cursor = cursor;
|
panel.SetCursor(cursor);
| -| Метод | Описание | -|-------|----------| -| `AddOnCursorIndexChange(Action)` / `RemoveOnCursorIndexChange(Action)` | Подписка на изменение позиции курсора | -| `AddOnSelectIndexChange(Action)` / `RemoveOnSelectIndexChange(Action)` | Подписка на изменение якоря выделения | -| `SetCursorIndex(int)` | Текущая позиция курсора | -| `SetSelectIndex(int)` | Текущий якорь выделения | -| `SetSelectable(bool)` | Можно ли выделять текст | -| `SetSelectAllOnFocus(bool)` | Выделять весь текст при фокусе | -| `SetSelectAllOnMouseUp(bool)` | Выделять весь текст при отпускании мыши | -| `SetDoubleClickSelectsWord(bool)` | Двойной клик выделяет слово | -| `SetTripleClickSelectsLine(bool)` | Тройной клик выделяет строку | +
-### BaseField\ +## Значения и события -```csharp -field.SetLabel("My Field"); -field.SetValue(42); -``` +### Значение поля -### BaseBoolField (Toggle) +| До — Unity API | После — FastTools | +|---|---| +|
var field = new IntegerField("Mana cost");
field.value = 42;
field.SetValueWithoutNotify(10);
|
var field = new IntegerField("Mana cost")
    .SetValue(42)
    .SetValue(10, notify: false);
| -```csharp -toggle - .SetLabel("Включено") - .SetText("Показать расширенные настройки") - .SetToggleOnLabelClick(true); -``` +### Подписка и отписка -| Метод | Описание | -|-------|----------| -| `SetText(string)` | Устанавливает текст рядом с чекбоксом | -| `SetLabel(string)` | Устанавливает label поля | -| `SetToggleOnLabelClick(bool)` | Переключать ли значение по клику на label | +| До — Unity API | После — FastTools | +|---|---| +|
field.RegisterValueChangedCallback(
    onChanged);

field.UnregisterValueChangedCallback(
    onChanged);
|
field.AddValueChanged(onChanged);

field.RemoveValueChanged(onChanged);
| -### INotifyValueChanged\ +
+Типы значений и интеграция с Unity.Mathematics -```csharp -field.SetValue(42, notify: false); // устанавливает значение без генерации ChangeEvent -field.AddValueChanged(evt => Debug.Log(evt.newValue)); -field.RemoveValueChanged(myCallback); -``` - -Типизированные перегрузки доступны для `int`, `uint`, `nint`, `nuint`, `long`, `ulong`, `short`, `ushort`, `byte`, `sbyte`, `float`, `double`, `decimal`, `char`, `string`, `bool`, `Color`, `Vector2/3/4`, `Vector2Int/3Int`, `Rect/RectInt`, `Bounds/BoundsInt`, `Hash128`, `GUID`, `Quaternion`, `Matrix4x4`, `Gradient`, `AnimationCurve`, `Delegate`, `Enum`, `Object`, `object`, плюс обобщённый fallback `SetValue`. +Типизированные перегрузки доступны для `int`, `uint`, `nint`, `nuint`, `long`, `ulong`, `short`, `ushort`, `byte`, `sbyte`, `float`, `double`, `decimal`, `char`, `string`, `bool`, `Color`, `Vector2/3/4`, `Vector2Int/3Int`, `Rect/RectInt`, `Bounds/BoundsInt`, `Hash128`, `GUID` (Unity 6.4+), `Quaternion`, `Matrix4x4`, `Gradient`, `AnimationCurve`, `Delegate`, `Enum`, `Object`, `object`, а также обобщённый вариант `SetValue` для остальных типов. > При установленном пакете `com.unity.mathematics` автоматически выставляется define `ASPID_FASTTOOLS_UNITY_MATHEMATICS_INTEGRATION` и добавляются перегрузки `SetValue` / `AddValueChanged` / `RemoveValueChanged` для `int2/3/4` (и `intMxN`), `float2/3/4` (и `floatMxN`), `half`/`half2/3/4`, `bool2/3/4` (и `boolMxN`), а также `quaternion`. -### IMixedValueSupport +
-```csharp -field.SetShowMixedValue(true); // показывает индикатор смешанного значения -``` +### Кнопки и манипуляторы -### Button +Обычный `VisualElement` тоже можно сделать кликабельным. Перегрузка с `out` позволяет сохранить манипулятор для удаления: ```csharp -button - .AddClicked(() => Debug.Log("Clicked")) - .SetClickable(new Clickable(() => { })) - .SetIconImage(myBackground); +panel.AddClickable(Refresh, out var clickable); + +// Когда клик больше не нужен +panel.RemoveManipulatorSelf(clickable); ``` -| Метод | Описание | -|-------|----------| -| `AddClicked(Action)` | Подписка на `Button.clicked` | -| `RemoveClicked(Action)` | Отписка от `Button.clicked` | -| `SetClickable(Clickable)` | Устанавливает `Button.clickable` | -| `SetIconImage(Background)` | Устанавливает `Button.iconImage` | +| До — Unity API | После — FastTools | +|---|---| +|
panel.AddManipulator(manipulator);
|
panel.AddManipulatorSelf(manipulator);
| +|
panel.RemoveManipulator(manipulator);
|
panel.RemoveManipulatorSelf(manipulator);
| +|
panel.AddManipulator(new Clickable(Refresh));
|
panel.AddClickable(Refresh);
| +|
var clickable = new Clickable(Refresh);
panel.AddManipulator(clickable);
|
panel.AddClickable(Refresh, out var clickable);
| +|
panel.AddManipulator(
    new Clickable(evt => Refresh()));
|
panel.AddClickable(evt => Refresh());
| +|
panel.AddManipulator(
    new Clickable(Refresh, delay: 500, interval: 100));
|
panel.AddClickable(
    Refresh, delay: 500, interval: 100);
| +|
panel.AddManipulator(
    new KeyboardNavigationManipulator(OnNavigate));
|
panel.AddKeyboardNavigationManipulator(OnNavigate);
| +|
panel.AddManipulator(
    new ContextualMenuManipulator(BuildMenu));
|
panel.AddContextualMenuManipulator(BuildMenu);
| + +## Конкретные элементы + +
+TextElement + +| До — Unity API | После — FastTools | +|---|---| +|
label.text = "Hello World";
|
label.SetText("Hello World");
| +|
label.enableRichText = true;
|
label.SetEnableRichText(true);
| +|
label.emojiFallbackSupport = true;
|
label.SetEmojiFallbackSupport(true);
| +|
label.parseEscapeSequences = true;
|
label.SetParseEscapeSequences(true);
| +|
label.displayTooltipWhenElided = true;
|
label.SetDisplayTooltipWhenElided(true);
| + +
+ +
+ITextEdition (TextField, IntegerField, …) + +| До — Unity API | После — FastTools | +|---|---| +|
textField.textEdition.maxLength = 64;
|
textField.textEdition.SetMaxLength(64);
| +|
textField.textEdition.maskChar = '*';
|
textField.textEdition.SetMaskChar('*');
| +|
textField.textEdition.isDelayed = true;
|
textField.textEdition.SetDelayed(true);
| +|
textField.textEdition.isReadOnly = true;
|
textField.textEdition.SetReadOnly(true);
| +|
textField.textEdition.isPassword = true;
|
textField.textEdition.SetPassword(true);
| +|
textField.textEdition.placeholder = "Поиск…";
|
textField.textEdition.SetPlaceholder("Поиск…");
| +|
textField.textEdition.autoCorrection = true;
|
textField.textEdition.SetAutoCorrection(true);
| +|
textField.textEdition.hideMobileInput = true;
|
textField.textEdition.SetHideMobileInput(true);
| +|
// Unity 6.4+
textField.textEdition.hideSoftKeyboard = true;
|
// Unity 6.4+
textField.textEdition.SetHideSoftKeyboard(true);
| +|
textField.textEdition.hidePlaceholderOnFocus = true;
|
textField.textEdition.SetHidePlaceholderOnFocus(true);
| +|
textField.textEdition.keyboardType =
    TouchScreenKeyboardType.NumberPad;
|
textField.textEdition.SetKeyboardType(
    TouchScreenKeyboardType.NumberPad);
| + +
+ +
+ITextSelection + +| До — Unity API | После — FastTools | +|---|---| +|
// Unity 6.3+
textField.textSelection.OnCursorIndexChange += OnCursor;
textField.textSelection.OnCursorIndexChange -= OnCursor;
|
// Unity 6.3+
textField.textSelection.AddOnCursorIndexChange(OnCursor);
textField.textSelection.RemoveOnCursorIndexChange(OnCursor);
| +|
// Unity 6.3+
textField.textSelection.OnSelectIndexChange += OnSelect;
textField.textSelection.OnSelectIndexChange -= OnSelect;
|
// Unity 6.3+
textField.textSelection.AddOnSelectIndexChange(OnSelect);
textField.textSelection.RemoveOnSelectIndexChange(OnSelect);
| +|
textField.textSelection.cursorIndex = 0;
|
textField.textSelection.SetCursorIndex(0);
| +|
textField.textSelection.selectIndex = 0;
|
textField.textSelection.SetSelectIndex(0);
| +|
textField.textSelection.isSelectable = true;
|
textField.textSelection.SetSelectable(true);
| +|
textField.textSelection.selectAllOnFocus = true;
|
textField.textSelection.SetSelectAllOnFocus(true);
| +|
textField.textSelection.selectAllOnMouseUp = true;
|
textField.textSelection.SetSelectAllOnMouseUp(true);
| +|
textField.textSelection.doubleClickSelectsWord = true;
|
textField.textSelection.SetDoubleClickSelectsWord(true);
| +|
textField.textSelection.tripleClickSelectsLine = true;
|
textField.textSelection.SetTripleClickSelectsLine(true);
| + +
+ +
+BaseField<TValueType> + +| До — Unity API | После — FastTools | +|---|---| +|
field.label = "Mana cost";
|
field.SetLabel("Mana cost");
| + +
+ +
+BaseBoolField (Toggle) + +| До — Unity API | После — FastTools | +|---|---| +|
toggle.text = "Показать расширенные настройки";
|
toggle.SetText("Показать расширенные настройки");
| +|
toggle.label = "Включено";
|
toggle.SetLabel("Включено");
| +|
toggle.toggleOnLabelClick = true;
|
toggle.SetToggleOnLabelClick(true);
| + +
+ +
+IMixedValueSupport + +| До — Unity API | После — FastTools | +|---|---| +|
field.showMixedValue = true;
|
field.SetShowMixedValue(true);
| + +
+ +
+Button + +| До — Unity API | После — FastTools | +|---|---| +|
button.clicked += Refresh;
|
button.AddClicked(Refresh);
| +|
button.clicked -= Refresh;
|
button.RemoveClicked(Refresh);
| +|
button.clickable = clickable;
|
button.SetClickable(clickable);
| +|
button.clickable = new Clickable(Refresh);
|
button.SetClickable(Refresh);
| +|
button.iconImage = iconImage;
|
button.SetIconImage(iconImage);
| + +
+ +
+Slider / BaseSlider<TValue> + +| До — Unity API | После — FastTools | +|---|---| +|
slider.lowValue = 0f;
|
slider.SetLowValue(0f);
| +|
slider.highValue = 100f;
|
slider.SetHighValue(100f);
| +|
slider.fill = true;
|
slider.SetFill(true);
| +|
slider.inverted = true;
|
slider.SetInverted(true);
| +|
slider.pageSize = 10f;
|
slider.SetPageSize(10f);
| +|
slider.showInputField = true;
|
slider.SetShowInputField(true);
| +|
slider.direction = SliderDirection.Vertical;
|
slider.SetDirection(SliderDirection.Vertical);
| + +
+ +
+ProgressBar + +| До — Unity API | После — FastTools | +|---|---| +|
progressBar.title = "Загрузка…";
|
progressBar.SetTitle("Загрузка…");
| +|
progressBar.lowValue = 0f;
|
progressBar.SetLowValue(0f);
| +|
progressBar.highValue = 100f;
|
progressBar.SetHighValue(100f);
| +|
progressBar.value = 42f;
|
progressBar.SetValue(42f);
| + +
+ +
+HelpBox + +| До — Unity API | После — FastTools | +|---|---| +|
helpBox.text = "Что-то пошло не так";
|
helpBox.SetText("Что-то пошло не так");
| +|
helpBox.messageType =
    HelpBoxMessageType.Warning;
|
helpBox.SetMessageType(
    HelpBoxMessageType.Warning);
| + +
+ +
+EnumField / EnumFlagsField + +| До — Unity API | После — FastTools | +|---|---| +|
enumField.Init(
    Mode.Default, includeObsoleteValues: false);
|
enumField.Initialize(
    Mode.Default, includeObsoleteValues: false);
| + +
+ +
+Foldout + +| До — Unity API | После — FastTools | +|---|---| +|
foldout.text = "Заголовок раздела";
|
foldout.SetText("Заголовок раздела");
| +|
foldout.toggleOnLabelClick = true;
|
foldout.SetToggleOnLabelClick(true);
| + +
+ +
+Image + +| До — Unity API | После — FastTools | +|---|---| +|
image.image = texture;
|
image.SetImage(texture);
| +|
image.image =
    Resources.Load<Texture>("UI/Icon");
|
image.SetImageFromResources("UI/Icon");
| +|
image.sprite = sprite;
|
image.SetSprite(sprite);
| +|
image.sprite =
    Resources.Load<Sprite>("UI/Icon");
|
image.SetSpriteFromResources("UI/Icon");
| +|
image.vectorImage = vectorImage;
|
image.SetVectorImage(vectorImage);
| +|
image.vectorImage =
    Resources.Load<VectorImage>("UI/Icon");
|
image.SetVectorImageFromResources("UI/Icon");
| +|
image.uv = new Rect(0, 0, 1, 1);
|
image.SetUv(new Rect(0, 0, 1, 1));
| +|
image.sourceRect = sourceRect;
|
image.SetSourceRect(sourceRect);
| +|
image.tintColor = Color.white;
|
image.SetTintColor(Color.white);
| +|
image.scaleMode = ScaleMode.ScaleToFit;
|
image.SetScaleMode(ScaleMode.ScaleToFit);
| + +
+ +
+IMGUIContainer + +| До — Unity API | После — FastTools | +|---|---| +|
container.onGUIHandler = OnGUI;
|
container.SetOnGUIHandler(OnGUI);
| +|
container.onGUIHandler += OnGUI;
|
container.AddOnGUIHandler(OnGUI);
| +|
container.onGUIHandler -= OnGUI;
|
container.RemoveOnGUIHandler(OnGUI);
| +|
container.cullingEnabled = true;
|
container.SetCullingEnabled(true);
| +|
container.contextType = ContextType.Editor;
|
container.SetContextType(ContextType.Editor);
| +|
container.MarkDirtyLayout();
|
container.MarkDirtyLayout();
| + +
+ +## Списки и деревья + +Общие настройки доступны на `ListView`, `TreeView` и их `MultiColumn`-вариантах. `SetMakeItem`, `SetBindItem`, `SetUnbindItem` и `SetDestroyItem` относятся к обычным `ListView` и `TreeView`. + +#### Данные и поведение BaseVerticalCollectionView + +| До — Unity API | После — FastTools | +|---|---| +|
listView.itemsSource = items;
|
listView.SetItemsSource(items);
| +|
listView.reorderable = true;
|
listView.SetReorderable(true);
| +|
listView.selectedIndex = 0;
|
listView.SetSelectedIndex(0);
| +|
listView.selectionType = SelectionType.Single;
|
listView.SetSelectionType(SelectionType.Single);
| +|
listView.fixedItemHeight = 24;
|
listView.SetFixedItemHeight(24);
| +|
listView.virtualizationMethod =
    CollectionVirtualizationMethod.DynamicHeight;
|
listView.SetVirtualizationMethod(
    CollectionVirtualizationMethod.DynamicHeight);
| +|
listView.horizontalScrollingEnabled = true;
|
listView.SetHorizontalScrollingEnabled(true);
| +|
listView.showAlternatingRowBackgrounds =
    AlternatingRowBackground.All;
|
listView.SetShowAlternatingRowBackgrounds(
    AlternatingRowBackground.All);
| + +#### События BaseVerticalCollectionView + +| До — Unity API | После — FastTools | +|---|---| +|
listView.itemsChosen += OnItemsChosen;
listView.itemsChosen -= OnItemsChosen;
|
listView.AddItemsChosen(OnItemsChosen);
listView.RemoveItemsChosen(OnItemsChosen);
| +|
listView.selectionChanged += OnSelectionChanged;
listView.selectionChanged -= OnSelectionChanged;
|
listView.AddSelectionChanged(OnSelectionChanged);
listView.RemoveSelectionChanged(OnSelectionChanged);
| +|
listView.selectedIndicesChanged += OnIndicesChanged;
listView.selectedIndicesChanged -= OnIndicesChanged;
|
listView.AddSelectedIndicesChanged(OnIndicesChanged);
listView.RemoveSelectedIndicesChanged(OnIndicesChanged);
| +|
listView.itemIndexChanged += OnItemMoved;
listView.itemIndexChanged -= OnItemMoved;
|
listView.AddItemIndexChanged(OnItemMoved);
listView.RemoveItemIndexChanged(OnItemMoved);
| +|
listView.itemsSourceChanged += OnSourceChanged;
listView.itemsSourceChanged -= OnSourceChanged;
|
listView.AddItemsSourceChanged(OnSourceChanged);
listView.RemoveItemsSourceChanged(OnSourceChanged);
| +|
listView.canStartDrag += CanStartDrag;
listView.canStartDrag -= CanStartDrag;
|
listView.AddCanStartDrag(CanStartDrag);
listView.RemoveCanStartDrag(CanStartDrag);
| +|
listView.setupDragAndDrop += SetupDrag;
listView.setupDragAndDrop -= SetupDrag;
|
listView.AddSetupDragAndDrop(SetupDrag);
listView.RemoveSetupDragAndDrop(SetupDrag);
| +|
listView.dragAndDropUpdate += UpdateDrag;
listView.dragAndDropUpdate -= UpdateDrag;
|
listView.AddDragAndDropUpdate(UpdateDrag);
listView.RemoveDragAndDropUpdate(UpdateDrag);
| +|
listView.handleDrop += HandleDrop;
listView.handleDrop -= HandleDrop;
|
listView.AddHandleDrop(HandleDrop);
listView.RemoveHandleDrop(HandleDrop);
| + +#### Настройка BaseListView + +| До — Unity API | После — FastTools | +|---|---| +|
listView.allowAdd = true;
listView.allowRemove = true;
|
listView.SetAllowAdd(true).SetAllowRemove(true);
| +|
listView.headerTitle = "Способности";
|
listView.SetHeaderTitle("Способности");
| +|
listView.showFoldoutHeader = true;
|
listView.SetShowFoldoutHeader(true);
| +|
listView.showAddRemoveFooter = true;
|
listView.SetShowAddRemoveFooter(true);
| +|
listView.showBoundCollectionSize = true;
|
listView.SetShowBoundCollectionSize(true);
| +|
listView.reorderMode = ListViewReorderMode.Animated;
|
listView.SetReorderMode(ListViewReorderMode.Animated);
| +|
listView.bindingSourceSelectionMode =
    BindingSourceSelectionMode.AutoAssign;
|
listView.SetBindingSourceSelectionMode(
    BindingSourceSelectionMode.AutoAssign);
| +|
listView.onAdd = OnAdd;
listView.onAdd += OnAdd;
listView.onAdd -= OnAdd;
|
listView.SetOnAdd(OnAdd);
listView.AddOnAdd(OnAdd);
listView.RemoveOnAdd(OnAdd);
| +|
listView.onRemove = OnRemove;
listView.onRemove += OnRemove;
listView.onRemove -= OnRemove;
|
listView.SetOnRemove(OnRemove);
listView.AddOnRemove(OnRemove);
listView.RemoveOnRemove(OnRemove);
| +|
listView.overridingAddButtonBehavior = OnAddButton;
listView.overridingAddButtonBehavior += OnAddButton;
listView.overridingAddButtonBehavior -= OnAddButton;
|
listView.SetOverridingAddButtonBehavior(OnAddButton);
listView.AddOverridingAddButtonBehavior(OnAddButton);
listView.RemoveOverridingAddButtonBehavior(OnAddButton);
| +|
listView.makeFooter = () => new Label();
|
listView.SetMakeFooter(() => new Label());
| +|
listView.makeHeader = () => new Label();
|
listView.SetMakeHeader(() => new Label());
| +|
listView.makeNoneElement =
    () => new Label("Способностей нет");
|
listView.SetMakeNoneElement(
    () => new Label("Способностей нет"));
| +|
listView.itemsAdded += OnItemsAdded;
listView.itemsAdded -= OnItemsAdded;
|
listView.AddItemsAdded(OnItemsAdded);
listView.RemoveItemsAdded(OnItemsAdded);
| +|
listView.itemsRemoved += OnItemsRemoved;
listView.itemsRemoved -= OnItemsRemoved;
|
listView.AddItemsRemoved(OnItemsRemoved);
listView.RemoveItemsRemoved(OnItemsRemoved);
| + +#### Настройка BaseTreeView + +| До — Unity API | После — FastTools | +|---|---| +|
treeView.autoExpand = true;
|
treeView.SetAutoExpand(true);
| +|
treeView.itemExpandedChanged += OnExpanded;
treeView.itemExpandedChanged -= OnExpanded;
|
treeView.AddItemExpandedChanged(OnExpanded);
treeView.RemoveItemExpandedChanged(OnExpanded);
| + +#### Создание элементов ListView и TreeView -### Slider / BaseSlider\ +Эти методы дублируются в `ListViewExtensions` и `TreeViewExtensions` (каждое работает со своим типом view). -```csharp -slider - .SetLowValue(0f) - .SetHighValue(100f) - .SetShowInputField(true); -``` +| До — Unity API | После — FastTools | +|---|---| +|
listView.makeItem = () => new Label();
|
listView.SetMakeItem(() => new Label());
| +|
listView.bindItem = BindRow;
listView.bindItem += BindRow;
listView.bindItem -= BindRow;
|
listView.SetBindItem(BindRow);
listView.AddBindItem(BindRow);
listView.RemoveBindItem(BindRow);
| +|
listView.unbindItem = UnbindRow;
listView.unbindItem += UnbindRow;
listView.unbindItem -= UnbindRow;
|
listView.SetUnbindItem(UnbindRow);
listView.AddUnbindItem(UnbindRow);
listView.RemoveUnbindItem(UnbindRow);
| +|
listView.destroyItem = DestroyRow;
listView.destroyItem += DestroyRow;
listView.destroyItem -= DestroyRow;
|
listView.SetDestroyItem(DestroyRow);
listView.AddDestroyItem(DestroyRow);
listView.RemoveDestroyItem(DestroyRow);
| +|
listView.itemTemplate = rowTemplate;
|
listView.SetItemTemplate(rowTemplate);
| -| Метод | Описание | -|-------|----------| -| `SetLowValue(TValue)` | Устанавливает минимальное значение слайдера | -| `SetHighValue(TValue)` | Устанавливает максимальное значение слайдера | -| `SetFill(bool)` | Заполнение трека до текущего значения | -| `SetInverted(bool)` | Инвертирует направление слайдера | -| `SetPageSize(float)` | Шаг изменения значения при постраничной навигации | -| `SetShowInputField(bool)` | Показывает числовое поле ввода рядом со слайдером | -| `SetDirection(SliderDirection)` | Устанавливает ориентацию слайдера | +#### `MultiColumnListView` / `MultiColumnTreeView` -### ProgressBar +| До — Unity API | После — FastTools | +|---|---| +|
listView.sortingMode = ColumnSortingMode.Default;
|
listView.SetSortingMode(ColumnSortingMode.Default);
| +|
listView.columnSortingChanged += OnSortingChanged;
listView.columnSortingChanged -= OnSortingChanged;
|
listView.AddColumnSortingChanged(OnSortingChanged);
listView.RemoveColumnSortingChanged(OnSortingChanged);
| -```csharp -progressBar.SetTitle("Загрузка...").SetLowValue(0f).SetHighValue(100f); -``` +## Расширения редактора -| Метод | Описание | -|-------|----------| -| `SetTitle(string)` | Устанавливает заголовок, отображаемый в центре | -| `SetLowValue(float)` | Устанавливает минимальное значение | -| `SetHighValue(float)` | Устанавливает максимальное значение | +Расширения выше работают в редакторе и в игре. Привязка к `SerializedObject` и редакторские команды лежат в сборке `Aspid.FastTools.UIElements.Editors`, поэтому такой код размещайте в editor-сборке, например в папке `Editor`. Добавьте в этот скрипт `using Aspid.FastTools.UIElements.Editors;` и `using UnityEditor.UIElements;`. -### HelpBox +### Привязка к SerializedObject -```csharp -helpBox - .SetText("Что-то пошло не так") - .SetMessageType(HelpBoxMessageType.Warning); -``` +| До — Unity API | После — FastTools | +|---|---| +|
field.bindingPath = "_manaCost";
field.Bind(serializedObject);
|
field.BindTo(
    serializedObject, "_manaCost");
| +|
var property = serializedObject
    .FindProperty("_manaCost");
field.BindProperty(property);
|
var property = serializedObject
    .FindProperty("_manaCost");
field.BindPropertyTo(property);
| +|
root.Bind(serializedObject);
|
root.BindTo(serializedObject);
| +|
root.Unbind();
|
root.UnbindFrom();
| -| Метод | Описание | -|-------|----------| -| `SetText(string)` | Текст сообщения help-box | -| `SetMessageType(HelpBoxMessageType)` | Иконка / уровень (`None` / `Info` / `Warning` / `Error`) | +### PropertyField -### Foldout +`PropertyField.AddValueChanged` получает `SerializedPropertyChangeEvent`. У обычного `IntegerField.AddValueChanged` аргументом будет `ChangeEvent`: ```csharp -foldout - .SetText("Section Title") - .SetToggleOnLabelClick(true) - .SetValue(true); +var manaCost = serializedObject.FindProperty("_manaCost"); +var field = new PropertyField(manaCost) + .SetLabel("Mana cost") + .AddValueChanged(evt => + Debug.Log(evt.changedProperty.intValue)); ``` -| Метод | Описание | -|-------|----------| -| `SetText(string)` | Заголовок foldout | -| `SetToggleOnLabelClick(bool)` | Переключать ли раскрытие по клику на заголовок | - -### Image +Для записи в свойство из собственного кода используйте [SerializedProperty Extensions](08-serialized-property-extensions.md). -```csharp -image - .SetImage(myTexture) - .SetTintColor(Color.white) - .SetScaleMode(ScaleMode.ScaleToFit); -``` +### Открытие скрипта и окно-владелец -| Метод | Описание | -|-------|----------| -| `SetImage(Texture)` | Устанавливает `Image.image` | -| `SetImageFromResources(string)` | Загружает текстуру через `Resources.Load` | -| `SetSprite(Sprite)` | Устанавливает `Image.sprite` | -| `SetSpriteFromResources(string)` | Загружает sprite через `Resources.Load` | -| `SetVectorImage(VectorImage)` | Устанавливает `Image.vectorImage` | -| `SetVectorImageFromResources(string)` | Загружает vector image через `Resources.Load` | -| `SetUv(Rect)` | Устанавливает UV-rect | -| `SetSourceRect(Rect)` | Устанавливает source rect | -| `SetTintColor(Color)` | Цветовой tint изображения | -| `SetScaleMode(ScaleMode)` | Режим масштабирования | - -### IMGUIContainer +Двойной клик по элементу открывает в IDE скрипт `target` — `MonoBehaviour` или `ScriptableObject`. ```csharp -container - .SetOnGUIHandler(() => GUILayout.Label("IMGUI")) - .SetCullingEnabled(true); +image.AddOpenScriptCommand(target); ``` -| Метод | Описание | -|-------|----------| -| `SetOnGUIHandler(Action)` | Заменяет коллбэк `onGUIHandler` | -| `AddOnGUIHandler(Action)` | Подписка на `onGUIHandler` | -| `RemoveOnGUIHandler(Action)` | Отписка от `onGUIHandler` | -| `SetCullingEnabled(bool)` | Пропускает `onGUIHandler`, когда элемент за пределами экрана | -| `SetContextType(ContextType)` | Устанавливает тип контекста IMGUI | -| `MarkDirtyLayout()` | Помечает IMGUI-layout как «грязный» для пересчёта | - -### Collection views (ListView, TreeView, MultiColumn variants) - -Общие методы распределены по нескольким специализированным расширениям: - -- `BaseVerticalCollectionViewExtensions` — применяется ко **всем** collection-views (ListView, TreeView, MultiColumn-варианты). -- `BaseListViewExtensions` — применяется к ListView и MultiColumnListView. -- `BaseTreeViewExtensions` — применяется к TreeView и MultiColumnTreeView. -- `ListViewExtensions` / `TreeViewExtensions` — фабрики `MakeItem`/`BindItem`/`UnbindItem`/`DestroyItem` для своего вью. -- `MultiColumnListViewExtensions` / `MultiColumnTreeViewExtensions` — хелперы для multi-column-вариантов. +`GetOwnerWindow()` ищет окно по панели элемента. Если найти его не удалось, возвращает окно в фокусе, затем окно под курсором; результат может быть `null`. Это полезно при позиционировании попапа, когда клик уже пришёл, а фокус ещё не переключился. ```csharp -listView - .SetItemsSource(items) - .SetMakeItem(() => new Label()) - .SetBindItem((el, i) => ((Label)el).SetText(items[i])) - .SetSelectionType(SelectionType.Single) - .AddSelectionChanged(selected => Debug.Log(selected)); +var window = image.GetOwnerWindow(); ``` -#### Source, layout and behavior — `BaseVerticalCollectionView` - -| Метод | Описание | -|-------|----------| -| `SetItemsSource(IList)` | Источник данных | -| `SetReorderable(bool)` | Включает drag-reorder | -| `SetSelectedIndex(int)` | Выбирает элемент по индексу | -| `SetSelectionType(SelectionType)` | None / Single / Multiple | -| `SetFixedItemHeight(float)` | Фиксированная высота элемента (для виртуализации `FixedHeight`) | -| `SetVirtualizationMethod(CollectionVirtualizationMethod)` | `FixedHeight` или `DynamicHeight` | -| `SetHorizontalScrollingEnabled(bool)` | Включает горизонтальную прокрутку | -| `SetShowAlternatingRowBackgrounds(AlternatingRowBackground)` | Режим зебра-полос | - -#### Events — `BaseVerticalCollectionView` - -| Метод | Описание | -|-------|----------| -| `AddItemsChosen(Action>)` / `RemoveItemsChosen` | Подтверждение элементов (двойной клик / Enter) | -| `AddSelectionChanged(Action>)` / `RemoveSelectionChanged` | Изменение выделения (объекты) | -| `AddSelectedIndicesChanged(Action>)` / `RemoveSelectedIndicesChanged` | Изменение выделения (индексы) | -| `AddItemIndexChanged(Action)` / `RemoveItemIndexChanged` | Перемещение элемента (drag-reorder) | -| `AddItemsSourceChanged(Action)` / `RemoveItemsSourceChanged` | Смена ссылки `itemsSource` | -| `AddCanStartDrag(Func)` / `RemoveCanStartDrag` | Кастомный gating старта drag | -| `AddSetupDragAndDrop(Func)` / `RemoveSetupDragAndDrop` | Подготовка drag-and-drop | -| `AddDragAndDropUpdate(Func)` / `RemoveDragAndDropUpdate` | Визуальный режим drag-and-drop | -| `AddHandleDrop(Func)` / `RemoveHandleDrop` | Обработка drop | - -#### `BaseListView`-specific - -| Метод | Описание | -|-------|----------| -| `SetAllowAdd(bool)` · `SetAllowRemove(bool)` | Включают встроенные кнопки add/remove | -| `SetHeaderTitle(string)` | Заголовок при включённом foldout-header | -| `SetShowFoldoutHeader(bool)` | Оборачивает список в `Foldout` | -| `SetShowAddRemoveFooter(bool)` | Показывает footer с add/remove | -| `SetShowBoundCollectionSize(bool)` | Поле размера коллекции | -| `SetReorderMode(ListViewReorderMode)` | `Simple` или `Animated` | -| `SetBindingSourceSelectionMode(BindingSourceSelectionMode)` | Auto-assign / manual | -| `SetOnAdd(Action)` · `AddOnAdd` · `RemoveOnAdd` | Кастомный коллбэк add-кнопки | -| `SetOnRemove(Action)` · `AddOnRemove` · `RemoveOnRemove` | Кастомный коллбэк remove-кнопки | -| `SetOverridingAddButtonBehavior(Action)` · `AddOverridingAddButtonBehavior` · `RemoveOverridingAddButtonBehavior` | Подменяет дефолтное поведение add | -| `SetMakeFooter(Func)` · `AddMakeFooter` · `RemoveMakeFooter` | Фабрика подвала (Unity 6+) | -| `SetMakeHeader(Func)` · `AddMakeHeader` · `RemoveMakeHeader` | Фабрика заголовка (Unity 6+) | -| `SetMakeNoneElement(Func)` · `AddMakeNoneElement` · `RemoveMakeNoneElement` | Фабрика empty-state (Unity 6+) | -| `AddItemsAdded(Action>)` / `RemoveItemsAdded` | Добавление элементов по индексам | -| `AddItemsRemoved(Action>)` / `RemoveItemsRemoved` | Удаление элементов по индексам | - -#### `BaseTreeView`-specific - -| Метод | Описание | -|-------|----------| -| `SetAutoExpand(bool)` | Авто-разворачивание новых узлов | -| `AddItemExpandedChanged(Action)` / `RemoveItemExpandedChanged` | Подписка на изменение раскрытия | - -#### `ListView` / `TreeView` item factories - -Эти методы дублируются в `ListViewExtensions` и `TreeViewExtensions` (каждое работает со своим типом view). - -| Метод | Описание | -|-------|----------| -| `SetMakeItem(Func)` · `AddMakeItem` · `RemoveMakeItem` | Фабрика элементов | -| `SetBindItem(Action)` · `AddBindItem` · `RemoveBindItem` | Привязка элемента | -| `SetUnbindItem(Action)` · `AddUnbindItem` · `RemoveUnbindItem` | Отвязка элемента | -| `SetDestroyItem(Action)` · `AddDestroyItem` · `RemoveDestroyItem` | Уничтожение элемента | -| `SetItemTemplate(VisualTreeAsset)` | UXML-шаблон, по которому строятся элементы | - -#### `MultiColumnListView` / `MultiColumnTreeView` - -| Метод | Описание | -|-------|----------| -| `SetSortingMode(ColumnSortingMode)` | Встроенный режим сортировки заголовка колонки | +## Собственные свойства USS -## Editor commands (editor-only) +Чтение строкового свойства USS как enum в `CustomStyleResolvedEvent`: -```csharp -using Aspid.FastTools.UIElements.Editors; +| До — Unity API | После — FastTools | +|---|---| +|
if (evt.customStyle.TryGetValue(ThemeProperty, out var raw)
    && Enum.TryParse(raw, ignoreCase: true, out PanelTheme theme))
    ApplyTheme(theme);
|
if (evt.customStyle.TryGetByEnum(ThemeProperty, out PanelTheme theme))
    ApplyTheme(theme);
| -image.AddOpenScriptCommand(target); -// Двойной клик на элемент открывает скрипт 'target' в IDE -``` +## Практический пример -| Метод | Цель | Описание | -|-------|------|----------| -| `AddOpenScriptCommand(Object)` | `VisualElement` | Регистрирует обработчик двойного клика, открывающий исходный скрипт `MonoBehaviour` / `ScriptableObject` в IDE. | -| `GetOwnerWindow()` | `VisualElement` | Возвращает `EditorWindow`, чья панель содержит элемент (для отсоединённых элементов — откат на окно в фокусе / под курсором). Используйте вместо `EditorWindow.focusedWindow` при привязке попапов к элементу — pointer-события приходят до переключения фокуса на кликнутое окно. | -| `BindTo(SerializedObject)` | `VisualElement` | Вызывает `BindingExtensions.Bind` на элементе. | -| `BindTo(SerializedObject, string propertyPath)` | `IBindable` | Устанавливает `bindingPath` и привязывается к указанному `SerializedObject`. | -| `BindPropertyTo(SerializedProperty)` | `IBindable` | Вызывает `BindingExtensions.BindProperty` для переданного property. | -| `Initialize(Enum defaultValue, bool includeObsoleteValues = false)` | `EnumField` / `EnumFlagsField` | Инициализирует поле указанным значением enum по умолчанию. | -| `AddValueChanged(EventCallback)` / `RemoveValueChanged(...)` | `PropertyField` | Подписка / отписка от уведомлений об изменении свойства. | +В [EditorTools](../../Samples~/EditorTools/Documentation/README.ru.md) собраны каталог способностей и реактивный инспектор, построенные на этих расширениях: -## USS custom-style helpers (`ICustomStyle`) +![Halve cooldown, +5 MP обновляет поля и описание эффекта; Undo возвращает прежние значения.](../../Samples~/EditorTools/Documentation/Images/demo.gif) -```csharp -using Aspid.FastTools.UIElements; - -private static readonly CustomStyleProperty ThemeProperty = new("--aspid-fasttools-prop-theme"); - -void OnCustomStyleResolved(CustomStyleResolvedEvent evt) -{ - if (evt.customStyle.TryGetByEnum(ThemeProperty, out ThemeStyle.Type theme)) - ApplyTheme(theme); -} -``` +Halve cooldown, +5 MP обновляет поля и описание эффекта; Undo возвращает прежние значения. -| Метод | Описание | -|-------|----------| -| `ICustomStyle.TryGetByEnum(CustomStyleProperty, out T)` | Резолвит USS custom-property со строковым значением и парсит её регистронезависимо как enum `T`. Используется во всех `*Style`-структурах с USS-driven enum (`ThemeStyle`, `StatusStyle`, `AspidLabelSizeStyle` и т. д.). | diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/08-serialized-property-extensions.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/08-serialized-property-extensions.md index e8ef3a1a..76898b5c 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/08-serialized-property-extensions.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/08-serialized-property-extensions.md @@ -1,124 +1,202 @@ # SerializedProperty Extensions -Цепочные методы расширения над `SerializedProperty` для синхронизации владеющего `SerializedObject`, установки значений и рефлексии над полем-источником. +Цепочечные методы расширения, с которыми `SerializedProperty` записывает и применяет своё значение одним вызовом, не обращаясь к своему `SerializedObject`. Вторая группа методов отвечает, какое поле C# и какой объект стоят за свойством. + +## Быстрый старт + +Примеры на этой странице работают с компонентом `AbilityBook`: ```csharp -using Aspid.FastTools.Editors; +using System; +using System.Collections.Generic; +using UnityEngine; + +public interface IAbilityEffect { } + +public enum Targeting { Single, Area } + +[Flags] +public enum DamageTypes { Fire = 1, Ice = 2, Poison = 4 } + +[Serializable] +public class BurnEffect : IAbilityEffect +{ + public float Damage = 5f; +} + +[Serializable] +public class Ability +{ + public string Name = "Fireball"; +} + +public class AbilityBook : MonoBehaviour +{ + [SerializeField] private int _manaCost = 10; + [SerializeField] private float _cooldown = 1f; + [SerializeField] private Sprite _icon; + [SerializeField] private Targeting _targeting; + [SerializeField] private DamageTypes _damageTypes; + [SerializeField] private List _abilities = new() { new Ability() }; + [SerializeReference] private IAbilityEffect _effect = new BurnEffect(); +} ``` -Все расширения обобщены по `T : SerializedProperty` и возвращают тот же экземпляр, поэтому вызовы можно свободно объединять в цепочки. +В его пользовательском `Editor` добавьте `using Aspid.FastTools.Editors;` и запишите значение поля: + +| До — Unity API | После — FastTools | +|---|---| +|
var manaCost = serializedObject
    .FindProperty("_manaCost");

serializedObject.Update();
manaCost.intValue = 42;
serializedObject
    .ApplyModifiedProperties();
|
var manaCost = serializedObject
    .FindProperty("_manaCost");

manaCost
    .Update()
    .SetIntAndApply(42);
| -## Update / Apply +Сеттеры, `Update()` и оба `Apply…()` возвращают исходное свойство, поэтому вызовы выстраиваются в цепочку. У каждого сеттера есть варианты `AndApply` (с Undo) и `AndApplyWithoutUndo`. -Тонкие обёртки над одноимёнными методами `SerializedObject` у `property.serializedObject`. +**Несколько полей:** обновите объект один раз, запишите значения и примените их вместе: ```csharp -property - .Update() - .SetInt(42) - .ApplyModifiedProperties(); +serializedObject.Update(); +serializedObject.FindProperty("_cooldown").SetFloat(0.5f); +serializedObject.FindProperty("_manaCost").SetIntAndApply(10); ``` -| Метод | Описание | -|-------|----------| -| `Update()` | Вызывает `serializedObject.Update()` | -| `UpdateIfRequiredOrScript()` | Вызывает `serializedObject.UpdateIfRequiredOrScript()` | -| `ApplyModifiedProperties()` | Вызывает `serializedObject.ApplyModifiedProperties()` | - -## SetValue / SetXxx — typed setters - -Для каждого поддерживаемого типа существуют четыре варианта: - -| Вариант | Поведение | -|---------|-----------| -| `SetValue(value)` | Обобщённый диспетчер — выбирает нужный типизированный сеттер по runtime-типу значения, возвращает `property` | -| `SetValueAndApply(value)` | `SetValue(value)` плюс `ApplyModifiedProperties()` | -| `SetXxx(value)` | Типизированный сеттер (например, `SetInt`), пишущий в соответствующее поле `SerializedProperty.xxxValue` | -| `SetXxxAndApply(value)` | `SetXxx(value)` плюс `ApplyModifiedProperties()` | - -### Supported types - -| Семейство методов | Unity-тип | Примечания | -|-------------------|-----------|------------| -| `SetInt` | `int` | | -| `SetUint` | `uint` | | -| `SetLong` | `long` | | -| `SetUlong` | `ulong` | | -| `SetFloat` | `float` | | -| `SetDouble` | `double` | | -| `SetBool` | `bool` | | -| `SetString` | `string` | | -| `SetColor` | `Color` | | -| `SetGradient` | `Gradient` | | -| `SetHash128` | `Hash128` | | -| `SetRect` / `SetRectInt` | `Rect` / `RectInt` | | -| `SetBounds` / `SetBoundsInt` | `Bounds` / `BoundsInt` | | -| `SetVector2` / `SetVector2Int` | `Vector2` / `Vector2Int` | | -| `SetVector3` / `SetVector3Int` | `Vector3` / `Vector3Int` | | -| `SetVector4` | `Vector4` | | -| `SetQuaternion` | `Quaternion` | | -| `SetAnimationCurve` | `AnimationCurve` | | -| `SetEntityId` | `EntityId` (`UnityEngine`) | Unity 6.2+ | - -### Enum setters - -Значения enum не идут через `SetValue` — используйте явную пару ниже в зависимости от того, является ли поле `[Flags]`-перечислением: - -| Метод | Описание | -|-------|----------| -| `SetEnumFlag(int)` / `SetEnumFlagAndApply(int)` | Пишет в `enumValueFlag` | -| `SetEnumIndex(int)` / `SetEnumIndexAndApply(int)` | Пишет в `enumValueIndex` | - -### Example +> [!IMPORTANT] +> Применение затрагивает **все накопленные изменения** связанного `SerializedObject`. +> `Update()` сбрасывает неприменённые записи — вызывайте его до изменения полей. + +## Обновление и применение + +Те же операции Unity, но с вызовом на свойстве: + +| До — Unity API | После — FastTools | +|---|---| +|
property.serializedObject
    .Update();
|
property.Update();
| +|
property.serializedObject
    .UpdateIfRequiredOrScript();
|
property
    .UpdateIfRequiredOrScript();
| +|
property.serializedObject
    .ApplyModifiedProperties();
|
property
    .ApplyModifiedProperties();
| +|
property.serializedObject
    .ApplyModifiedPropertiesWithoutUndo();
|
property
    .ApplyModifiedPropertiesWithoutUndo();
| + +## Запись значений + +Выберите, когда применять запись: + +| До — Unity API | После — FastTools | +|---|---| +|
// Применить позже
manaCost.intValue = 42;
|
// Применить позже
manaCost.SetInt(42);
| +|
// С Undo
manaCost.intValue = 42;
manaCost.serializedObject
    .ApplyModifiedProperties();
|
// С Undo
manaCost.SetIntAndApply(42);
| +|
// Без Undo
manaCost.intValue = 42;
manaCost.serializedObject
    .ApplyModifiedPropertiesWithoutUndo();
|
// Без Undo
manaCost
    .SetIntAndApplyWithoutUndo(42);
| + +`SetValue` — альтернатива явному сеттеру: `SetValue(42)` эквивалентен `SetInt(42)`, а `SetValue(0.5f)` — `SetFloat(0.5f)`. Перегрузка выбирается **по типу аргумента**, который должен соответствовать типу поля. + +### Поддерживаемые типы + +Явный сеттер и перегрузка `SetValue` есть для каждого типа значения `SerializedProperty`: + +| Значения | Сеттеры | +|---|---| +| Числа | `SetInt`, `SetUint`, `SetLong`, `SetUlong`, `SetFloat`, `SetDouble` | +| Текст, bool и хэш | `SetString`, `SetBool`, `SetHash128` | +| Векторы | `SetVector2`, `SetVector2Int`, `SetVector3`, `SetVector3Int`, `SetVector4`, `SetQuaternion` | +| Области | `SetRect`, `SetRectInt`, `SetBounds`, `SetBoundsInt` | +| Типы Unity | `SetColor`, `SetGradient`, `SetAnimationCurve` | +| Unity 6.2 и новее | `SetEntityId` для `UnityEngine.EntityId` | + +### Перечисления + +Для свойств `_targeting` и `_damageTypes`: + +| До — Unity API | После — FastTools | +|---|---| +|
// Targeting.Area
targeting.enumValueIndex = 1;
|
// Targeting.Area
targeting.SetEnumIndex(1);
| +|
// Fire | Ice
damageTypes.enumValueFlag = 3;
|
// Fire | Ice
damageTypes.SetEnumFlag(3);
| + +### Массивы и списки + +Для свойства коллекции `_abilities`: + +| До — Unity API | После — FastTools | +|---|---| +|
abilities.arraySize = 5;
abilities.arraySize += 1;
abilities.arraySize += 2;
abilities.arraySize -= 2;
abilities.arraySize -= 1;
|
abilities.SetArraySize(5);
abilities.AddArraySize();     // +1
abilities.AddArraySize(2);    // +2
abilities.RemoveArraySize(2); // -2
abilities.RemoveArraySize();  // -1
| + +Методы меняют только размер коллекции. `RemoveArraySize` убирает элементы с конца; новые элементы инициализируйте отдельно через `GetArrayElementAtIndex()`. + +### Ссылки и boxed-значения + +Выберите сеттер по способу сериализации поля: + +| До — Unity API | После — FastTools | +|---|---| +|
// [SerializeReference]
effect.managedReferenceValue = instance;
|
// [SerializeReference]
effect.SetManagedReference(instance);
| +|
// UnityEngine.Object
icon.objectReferenceValue = sprite;
|
// UnityEngine.Object
icon.SetObjectReference(sprite);
| +|
// ExposedReference<T>
property.exposedReferenceValue = target;
|
// ExposedReference<T>
property.SetExposedReference(target);
| +|
// boxedValue
property.boxedValue = value;
|
// boxedValue
property.SetBoxed(value);
| + +## Тип поля и объект-владелец + +Три метода через рефлексию находят поле C#, стоящее за свойством. Для `AbilityBook` из быстрого старта: ```csharp -SerializedProperty property = GetProperty(); - -// Эквивалентные формы -property.SetValue(10).ApplyModifiedProperties(); -property.SetValueAndApply(10); -property.SetInt(10).ApplyModifiedProperties(); -property.SetIntAndApply(10); - -// Цепочка из нескольких сеттеров -property - .SetVector3(Vector3.up) - .SetBool(true) - .ApplyModifiedProperties(); +var abilities = serializedObject.FindProperty("_abilities"); +var ability = abilities.GetArrayElementAtIndex(0); +var abilityName = ability.FindPropertyRelative("Name"); +var effect = serializedObject.FindProperty("_effect"); +var effectDamage = effect.FindPropertyRelative("Damage"); ``` -## Array operations +| Свойство | `GetPropertyType()` | `GetFieldInfo()` | `GetDeclaringInstance()` | +|---|---|---|---| +| `abilities` | `List` | `AbilityBook._abilities` | экземпляр `AbilityBook` | +| `ability` | `Ability` | `AbilityBook._abilities` | экземпляр `AbilityBook` | +| `abilityName` | `string` | `Ability.Name` | `Ability` с индексом 0 | +| `effect` | `IAbilityEffect` | `AbilityBook._effect` | экземпляр `AbilityBook` | +| `effectDamage` | `float` | `BurnEffect.Damage` | экземпляр `BurnEffect` | + +- `GetPropertyType()` возвращает **объявленный** тип поля: для `[SerializeReference]` — интерфейс или базовый класс, а не тип экземпляра; для элемента коллекции — тип элемента. +- `GetFieldInfo()` ищет поле по фактическому типу владельца, включая приватные поля базовых классов. +- `GetDeclaringInstance()` возвращает объект, которому принадлежит поле; для элемента коллекции — владельца коллекции. + +Все три метода возвращают `null`, если поиск не удался: поле не найдено, на пути встретилась `null`-ссылка или индекс вышел за границы списка. Они читают **первый** целевой объект (`targetObject`) и видят только применённые значения, поэтому сначала примените накопленные записи. + +> [!WARNING] +> Если владелец поля — структура, `GetDeclaringInstance()` возвращает её boxed-копию. Изменения такой копии не попадают в оригинал; записывайте значения через `SerializedProperty`. + +## Имя поля и проверка свойства + +Для тех же свойств: -| Метод | Описание | -|-------|----------| -| `SetArraySize(int)` / `SetArraySizeAndApply(int)` | Устанавливает `property.arraySize` | -| `AddArraySize(int = 1)` / `AddArraySizeAndApply(int = 1)` | Увеличивает `arraySize` на указанное количество (по умолчанию `1`) | -| `RemoveArraySize(int = 1)` / `RemoveArraySizeAndApply(int = 1)` | Уменьшает `arraySize` на указанное количество (по умолчанию `1`) | +| Вызов | Результат | +|---|---| +| `ability.GetMemberName()` | `"_abilities"` — имя коллекции без индекса | +| `abilityName.GetMemberName()` | `"Name"` | +| `ability.IsArrayElement()` | `true` | +| `abilityName.IsArrayElement()` | `false` — поле внутри элемента | +| `ability.HasFoldout()` | `true` | +| `abilityName.HasFoldout()` | `false` | +| `effect.HasFoldout()` | `false` — `[SerializeReference]` | -## Reference setters +`HasFoldout()` — это `true` только для свойства `Generic` с видимыми дочерними свойствами, как у стандартного инспектора. `[SerializeReference]` и пользовательские `PropertyDrawer` не учитываются. -| Метод | Описание | Примечания | -|-------|----------|------------| -| `SetManagedReference(object)` / `SetManagedReferenceAndApply(object)` | Пишет в `managedReferenceValue` (поле должно быть помечено `[SerializeReference]`) | | -| `SetObjectReference(Object)` / `SetObjectReferenceAndApply(Object)` | Пишет в `objectReferenceValue` | | -| `SetExposedReference(Object)` / `SetExposedReferenceAndApply(Object)` | Пишет в `exposedReferenceValue` | | -| `SetBoxed(object)` / `SetBoxedAndApply(object)` | Пишет в `boxedValue` | Unity 6+ | +## Независимое свойство -## Reflection helpers +`SerializedObject` инспектора живёт, пока открыт инспектор, поэтому свойство нельзя сохранить для отложенного вызова. `Persistent()` возвращает то же свойство на новом `SerializedObject` для тех же целевых объектов: -Для drawer-/inspector-кода, которому нужно получить runtime-тип или экземпляр, стоящий за property: +| До — Unity API | После — FastTools | +|---|---| +|
var independentObject =
    new SerializedObject(property
        .serializedObject.targetObjects);
var independent = independentObject
    .FindProperty(property.propertyPath);
|
var independent = property.Persistent();
| -| Метод | Возвращает | Описание | -|-------|------------|----------| -| `GetPropertyType()` | `Type` или `null` | Возвращает `FieldType` поля, стоящего за property (для элемента массива/списка — тип элемента). `null`, если поле не удаётся разрешить. | -| `GetFieldInfo()` | `FieldInfo` или `null` | Находит backing-поле, разрешая экземпляр-владелец property (`GetDeclaringInstance`) и ища поле на его runtime-типе, включая базовые классы — поэтому цепочка с `[SerializeReference]` разрешается естественно. Для элемента массива/списка возвращается поле коллекции (как `PropertyDrawer.fieldInfo`). | -| `GetDeclaringInstance()` | `object` или `null` | Идёт по `propertyPath` от корневого `targetObject` и возвращает runtime-экземпляр, на котором объявлено backing-поле property (для элемента массива/списка — владелец поля коллекции). `null`, если путь не удаётся разрешить. Владелец-структура возвращается как boxed-копия — изменения в ней не попадут в сериализуемый объект. | +Новый объект принадлежит вызывающему коду: освободите его и свойство после записи. ```csharp -public override void OnGUI(Rect rect, SerializedProperty property, GUIContent label) +var independent = manaCost.Persistent(); +if (independent == null) return; + +EditorApplication.delayCall += () => { - var declaringType = property.GetPropertyType(); - var owner = property.GetDeclaringInstance(); - // … -} + using (independent.serializedObject) + using (independent) + independent.Update().SetIntAndApply(42); +}; ``` + +`Persistent()` возвращает `null`, если путь свойства больше не существует. Неприменённые записи исходного объекта не копируются; исходное представление увидит изменения после `Update()`. Целевые объекты должны существовать до момента отложенного вызова. + +## Пример в пакете + +В [EditorTools](../../Samples~/EditorTools/Documentation/README.ru.md) кнопка **Halve cooldown, +5 MP** записывает два свойства через `SetFloat` и `SetIntAndApply` одним шагом Undo. diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/09-editor-helpers.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/09-editor-helpers.md index 17337cf7..0badfd53 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/09-editor-helpers.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/09-editor-helpers.md @@ -1,27 +1,55 @@ # Editor Helpers -Хелперы отображаемых имён объектов Unity для кастомных редакторов: +`GetDisplayName()` превращает имя типа объекта в читаемую подпись: `FireAbility` → «Fire Ability». `GetDisplayNameWithIndex()` добавляет номер, когда на GameObject несколько одинаковых компонентов: «Fire Ability (2)». -| Метод | Возвращает | -|---|---| -| `GetScriptName()` | Отображаемое имя объекта — `ObjectNames.GetInspectorTitle`, если у типа есть `[AddComponentMenu]`, иначе «очеловеченное» имя типа | -| `GetScriptNameWithIndex()` | То же имя с числовым суффиксом, когда на GameObject несколько компонентов одного типа — например `"Audio Source (2)"` | +![Результаты методов в пользовательском окне Unity. Заголовки стандартного Inspector эти методы не меняют.](../Images/editor-display-names.png) + +Результаты методов в пользовательском окне Unity. Заголовки стандартного Inspector эти методы не меняют. + +## Быстрый старт + +Примеры построены на двух компонентах: один задаёт себе имя через `[AddComponentMenu]`, другой — нет. ```csharp -using Aspid.FastTools.Editors; +using UnityEngine; -[CustomEditor(typeof(MyBehaviour))] -public class MyBehaviourEditor : Editor -{ - public override VisualElement CreateInspectorGUI() - { - // "My Behaviour" — или "Custom Name", если присутствует [AddComponentMenu("Custom Name")] - var name = target.GetScriptName(); +[AddComponentMenu("Gameplay/Fire Ability")] +public sealed class FireAbility : MonoBehaviour { } + +public sealed class AbilityConfig : MonoBehaviour { } +``` + +```csharp +using Aspid.FastTools.Editors; - // "My Behaviour (2)" при наличии второго компонента того же типа - var nameWithIndex = ((Component)target).GetScriptNameWithIndex(); +fireAbility.GetDisplayName(); // "Fire Ability" +abilityConfig.GetDisplayName(); // "Ability Config" - return new Label(name); - } -} +// Второй AbilityConfig на том же GameObject +abilityConfig.GetDisplayNameWithIndex(); // "Ability Config (2)" ``` + +> [!NOTE] +> Методы доступны только в редакторе. Размещайте использующий их код в папке `Editor` или в сборке, ограниченной платформой Editor. + +## GetDisplayName() + +`GetDisplayName()` работает с `UnityEngine.Object`. Если у типа есть `[AddComponentMenu]`, метод берёт заголовок через `ObjectNames.GetInspectorTitle`. В остальных случаях он преобразует имя типа через `ObjectNames.NicifyVariableName`. Для `null` или уничтоженного объекта метод возвращает `string.Empty`. + +| Компонент | `GetDisplayName()` | `ObjectNames.GetInspectorTitle()` | +|---|---|---| +| `FireAbility`, с атрибутом | `Fire Ability` | `Fire Ability` | +| `AbilityConfig`, без атрибута | `Ability Config` | `Ability Config (Script)` | + +## GetDisplayNameWithIndex() + +`GetDisplayNameWithIndex()` работает с `Component` и учитывает только компоненты **точно того же типа** на том же GameObject. Суффикс соответствует порядку компонентов, начиная с единицы. Для `null` или уничтоженного компонента метод возвращает `string.Empty`. + +| Компоненты на GameObject | Подписи | +|---|---| +| `AbilityConfig` | `Ability Config` | +| `AbilityConfig`, `AbilityConfig` | `Ability Config (1)`, `Ability Config (2)` | + +## Пример в пакете + +В [EditorTools](../../Samples~/EditorTools/Documentation/README.ru.md) метод `GetDisplayName()` формирует заголовок панели выбранной способности. diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/10-claude-code-plugin.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/10-claude-code-plugin.md index 3123acd0..7c9ea63d 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/10-claude-code-plugin.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/10-claude-code-plugin.md @@ -1,21 +1,43 @@ # Claude Code Plugin -Если вы используете [Claude Code](https://docs.claude.com/en/docs/claude-code), сопутствующий маркетплейс [Aspid.Claude.Plugins](https://github.com/VPDPersonal/Aspid.Claude.Plugins) поставляет плагин `aspid-fasttools` — набор скиллов, которые обучают Claude Code конвенциям и API этого пакета. +`aspid-fasttools` добавляет в [Claude Code](https://docs.claude.com/en/docs/claude-code) скиллы для профилирования методов и построения UI через fluent-расширения `VisualElement` из пакета. -> [!WARNING] -> Плагин всё ещё находится в бета-версии — его скиллы и команды могут меняться между релизами. +## Быстрый старт -Добавьте маркетплейс и установите плагин: +[Установите Aspid.FastTools](README.md#установка) в Unity-проект и откройте проект в Claude Code. В сессии Claude Code добавьте маркетплейс, затем установите плагин: -```sh +```text /plugin marketplace add VPDPersonal/Aspid.Claude.Plugins ``` -```sh +```text /plugin install aspid-fasttools@aspid-claude-plugins ``` -Включённые скиллы: +Плагин устанавливается отдельно от Unity-пакета. Откройте `/plugin`, чтобы проверить наличие `aspid-fasttools` среди установленных плагинов. -- **`aspid-profiler-marker`** — вставляет вызовы `this.Marker()` с правильной формой `using`/scope. -- **`aspid-visual-element-fluent`** — собирает editor- или runtime-UI через fluent-[расширения `VisualElement`](07-visual-element-extensions.md). +## Скиллы + +Скиллы активируются автоматически при подходящих запросах. Эти два покрывают возможности, описанные в документации пакета: + +| Скилл | Задача | Руководство по API | +|---|---|---| +| `aspid-profiler-marker` | Добавление областей замера методов и блоков через `this.Marker()` | [ProfilerMarkers](05-profiler-markers.md) | +| `aspid-visual-element-fluent` | Построение и оформление элементов UI Toolkit в C# | [VisualElement Extensions](07-visual-element-extensions.md) | + +Например, выделите метод и попросите: + +```text +Добавь маркер на весь метод Simulate и отдельный +именованный маркер на поиск соседей. +Используй this.Marker() из Aspid.FastTools. +``` + +После применения изменений проверьте компиляцию и маркеры в Unity Profiler. + +## Совместимость + +> [!IMPORTANT] +> Плагин находится в alpha. Его [документация](https://github.com/VPDPersonal/Aspid.Claude.Plugins/blob/main/plugins/aspid-fasttools/README_RU.md) ориентирована на прежний пакет `com.aspid.fasttools`, а эти руководства — на `tech.aspid.fasttools`. Сверяйте предлагаемый код с API установленного пакета. + +Плагин также содержит `aspid-id-struct` для API `IId` и `[UniqueId]` прежнего пакета, которые не входят в эту документацию. Плагин выпускается независимо; см. [релизы и обновления](https://github.com/VPDPersonal/Aspid.Claude.Plugins/releases). diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/11-component-type-selector.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/11-component-type-selector.md new file mode 100644 index 00000000..9b408805 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/11-component-type-selector.md @@ -0,0 +1,36 @@ +# ComponentTypeSelector + +`ComponentTypeSelector` меняет тип уже добавленного компонента или ScriptableObject через инспектор. При переключении между наследниками значения общих полей сохраняются. + +## Быстрый старт + +Добавьте поле в базовый класс. В селекторе появятся совместимые конкретные типы; пункта `` нет. + +```csharp +using UnityEngine; +using Aspid.FastTools.Types; + +public abstract class EnemyBase : MonoBehaviour +{ + [SerializeField] private ComponentTypeSelector _enemyType; + [SerializeField, Min(0)] private float _health = 100f; +} +``` + +Сохраните базу в `EnemyBase.cs`. Создайте наследников в **отдельных файлах**, совпадающих с именами классов: + +| FastEnemy.cs | ArmoredEnemy.cs | +|---|---| +|
using UnityEngine;

public sealed class FastEnemy : EnemyBase
{
    [SerializeField] private float _speed = 25f;
}
|
using UnityEngine;

public sealed class ArmoredEnemy : EnemyBase
{
    [SerializeField] private int _armor = 10;
}
| + +Добавьте **FastEnemy** на GameObject, задайте **Health = 75** и через селектор выберите **ArmoredEnemy**. Общий `Health` сохранится, поле `Speed` исчезнет, появится `Armor`. Уникальные поля прежнего класса не следует считать сохранёнными для обратного переключения. + +![Смена типа компонента через ComponentTypeSelector](../Images/component-type-selector.gif) + +Смена типа компонента через ComponentTypeSelector + +Выбранный класс должен иметь собственный файл скрипта, который распознаёт Unity. Если подходящий скрипт не найден, тип не меняется, а Console показывает предупреждение. + +## Пример в пакете + +Переключение типа компонента показано в примере [Types](../../Samples~/Types/Documentation/README.ru.md). diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/01-getting-started.md.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/11-component-type-selector.md.meta similarity index 75% rename from Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/01-getting-started.md.meta rename to Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/11-component-type-selector.md.meta index 611be6c9..85074e39 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/01-getting-started.md.meta +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/11-component-type-selector.md.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 90aaca0d316f48eb89debdf00fb0e797 +guid: e87c38aaa6f54b72a35f6fb35caecd02 TextScriptImporter: externalObjects: {} userData: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/README.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/README.md index 2c375d35..df60f81e 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/README.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Documentation/ru/README.md @@ -1,33 +1,131 @@ Aspid.FastTools -# Введение +[![Unity 6.0+](../Images/status-badge-unity.svg)](https://assetstore.unity.com/packages/slug/365584) +[![Preview 1.0.0-rc.8](../Images/status-badge-preview.svg)](https://github.com/VPDPersonal/Aspid.FastTools/releases) +[![MIT License](../Images/status-badge-license.svg)](https://github.com/VPDPersonal/Aspid.FastTools/blob/main/LICENSE) -**Aspid.FastTools** — набор инструментов для Unity, избавляющий от рутинного бойлерплейта. Внутри — удобная работа с `SerializeReference` (выбор типа в инспекторе и окно аудита ссылок по всему проекту), Roslyn-генераторы и анализаторы, а также runtime- и editor-утилиты: от сериализуемого `System.Type` до fluent-расширений UI Toolkit. +Aspid.FastTools — пакет для Unity, закрывающий пробелы в сериализации и редакторских инструментах. Сериализованные типы и полиморфные ссылки переживают переименования, а если ломаются, восстанавливаются без потери данных. Inspector показывает, что лежит в поле `SerializeReference`, и позволяет это заменить. Редакторские и профилировочные хелперы укладываются в строку там, где Unity требует класс. -[Исходный код](https://github.com/VPDPersonal/Aspid.FastTools) · [Unity Asset Store](https://assetstore.unity.com/packages/slug/365584) · [Releases](https://github.com/VPDPersonal/Aspid.FastTools/releases) +[Документация](https://vpdpersonal.github.io/Aspid.FastTools/ru/docs) · [Исходный код](https://github.com/VPDPersonal/Aspid.FastTools) · [Релизы](https://github.com/VPDPersonal/Aspid.FastTools/releases) -## Начало работы +## Установка -[Установка](01-getting-started.md) — UPM git URL, `.unitypackage`, Asset Store и примеры, поставляемые с пакетом. +В **Window → Package Manager** выберите **+ → Install package from git URL…** и вставьте: + +```text +https://github.com/VPDPersonal/Aspid.FastTools.git#upm-preview +``` + +Этот URL устанавливает последнюю preview-версию, а при обновлении пакета подтянется более новая. Для установки через Git URL в системе должен быть установлен Git. + +
+Другие варианты установки + +- **Другая версия:** скопируйте её UPM-тег со страницы [релизов](https://github.com/VPDPersonal/Aspid.FastTools/releases), например: + + ```text + https://github.com/VPDPersonal/Aspid.FastTools.git#upm-preview/1.0.0-rc.7 + ``` + +- **Unity Asset Store:** пакет пока недоступен в магазине. Для установки используйте Git URL выше. +- **Ветка `upm`:** всё ещё содержит старый пакет `com.aspid.fasttools` (`1.0.0-rc.2`). Для `tech.aspid.fasttools` и описанных здесь возможностей используйте URL выше. + +
## Возможности -| Возможность | Что даёт | -|---|---| -| [Serializable Type System](02-serializable-types.md) | `System.Type` как сериализуемое поле, `[TypeSelector]`, окно выбора типа с поиском, `ComponentTypeSelector` | -| [SerializeReference Selector](03-serialize-reference-selector.md) | Выпадающий выбор типа для полей `[SerializeReference]`, вложенные инспекторы, generics, точечная починка битых ссылок | -| [SerializeReference Tooling](04-serialize-reference-tooling.md) | Аудит и массовая починка по всему проекту, настройки проекта, build/CI-гейт | -| [ProfilerMarkers](05-profiler-markers.md) | Source-generated `ProfilerMarker`, уникальные для каждого места вызова, через `this.Marker()` | -| [EnumValues](06-enum-values.md) | Сериализуемые отображения enum → значение с поддержкой `[Flags]`, без boxing | -| [VisualElement Extensions](07-visual-element-extensions.md) | Fluent-построение UI Toolkit-деревьев в коде | -| [SerializedProperty Extensions](08-serialized-property-extensions.md) | Типизированные сеттеры с fluent-цепочками и рефлексионные хелперы | -| [Editor Helpers](09-editor-helpers.md) | Отображаемые имена скриптов для кастомных редакторов | -| [Claude Code Plugin](10-claude-code-plugin.md) | Скиллы, обучающие Claude Code этому пакету | +### [Serializable Type System](02-serializable-types.md) + +Хранение и выбор `System.Type` в инспекторе. + +Выбор сериализуемого типа в инспекторе + +### [ComponentTypeSelector](11-component-type-selector.md) + +Смена типа существующего компонента с сохранением общих полей. + +Смена типа компонента в инспекторе + +### [SerializeReference Selector](03-serialize-reference-selector.md) + +Выбор класса для поля `SerializeReference` прямо в инспекторе. + +Смена Pistol на Shotgun с сохранением Damage = 37 + +### [SerializeReference Tooling](04-serialize-reference-tooling.md) + +Аудит и восстановление ссылок по всему проекту, в том числе перед сборкой и в CI. + +Восстановление потерянного типа оружия с сохранением данных + +### [EnumValues](06-enum-values.md) + +Редактирование таблиц enum → значение в инспекторе, включая флаги. + +Редактирование enum-ключей и значений в инспекторе + +### [ProfilerMarkers](05-profiler-markers.md) + +Уникальный маркер профилирования для каждого места вызова через `this.Marker()`. + +```csharp +using (this.Marker()) +{ + Simulate(); +} +``` + +### [VisualElement Extensions](07-visual-element-extensions.md) + +Построение деревьев UI Toolkit fluent-цепочками. + +```csharp +new VisualElement() + .SetPadding(8) + .AddChild( + new Label("Stats")); +``` + +### [SerializedProperty Extensions](08-serialized-property-extensions.md) + +Запись значений, изменение размера массивов, получение типа поля и объекта-владельца. + +```csharp +property + .Update() + .SetIntAndApply(42); +``` + +### [Editor Helpers](09-editor-helpers.md) + +Читаемые подписи объектов и компонентов для редакторских инструментов. + +```csharp +audio.GetDisplayName(); +// "Audio Source" + +secondAudio + .GetDisplayNameWithIndex(); +// "Audio Source (2)" +``` + +## Быстрый старт + +1. После установки окно **Welcome** откроется само. Позже его можно открыть через **Tools → Aspid 🐍 → FastTools → Welcome**. +2. Нажмите **Import** у нужного примера; он появится в `Assets/Samples`. +3. Откройте его сцену и README. + +## Документация и примеры + +- [Обзор примеров](../../Samples~/README.ru.md) — сцены и инструменты для сериализации, enum-таблиц, профилирования и интерфейсов редактора. +- [Справочник API](https://vpdpersonal.github.io/Aspid.FastTools/ru/api/Aspid.FastTools) — публичные типы и члены. Ссылки в разделе возможностей выше ведут к руководствам по их использованию. +- [Плагин Claude Code](10-claude-code-plugin.md) — дополнительные скиллы для работы с пакетом в Claude Code. +- [Журнал изменений](https://github.com/VPDPersonal/Aspid.FastTools/blob/main/CHANGELOG.ru.md) — история релизов. -## Поддержать проект +## Помощь и поддержка -Этот проект разрабатывается на добровольной основе. Если он оказался для вас полезным, поддержать его развитие можно покупкой пакета в [Unity Asset Store](https://assetstore.unity.com/packages/slug/365584) — это помогает уделять больше времени улучшению и сопровождению **Aspid.FastTools**. +Сообщайте об ошибках и задавайте вопросы в [GitHub Issues](https://github.com/VPDPersonal/Aspid.FastTools/issues). Для ошибки укажите версию Unity, версию пакета и шаги воспроизведения. -## Лицензия +После публикации в [Unity Asset Store](https://assetstore.unity.com/packages/slug/365584) вы сможете поддержать разработку покупкой пакета. -**Aspid.FastTools** распространяется по [лицензии MIT](https://github.com/VPDPersonal/Aspid.FastTools/blob/main/LICENSE). История релизов — в [CHANGELOG](https://github.com/VPDPersonal/Aspid.FastTools/blob/main/CHANGELOG.ru.md). +Распространяется по [лицензии MIT](https://github.com/VPDPersonal/Aspid.FastTools/blob/main/LICENSE). diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Editor/Scripts/Extensions/EditorExtensions.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Editor/Scripts/Extensions/EditorExtensions.cs index 67974bf2..f087a1c5 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Editor/Scripts/Extensions/EditorExtensions.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Editor/Scripts/Extensions/EditorExtensions.cs @@ -1,7 +1,7 @@ using System; -using System.Linq; using UnityEditor; using UnityEngine; +using UnityEngine.Pool; using Object = UnityEngine.Object; // ReSharper disable CheckNamespace @@ -17,7 +17,7 @@ public static class EditorExtensions /// /// The object whose display name to resolve. /// The display name; otherwise, if is or destroyed. - public static string GetScriptName(this Object obj) + public static string GetDisplayName(this Object obj) { if (!obj) return string.Empty; @@ -31,26 +31,31 @@ public static string GetScriptName(this Object obj) /// Returns the component display name with a one-based suffix when its object has multiple components of the exact same type. /// /// The component whose indexed display name to resolve. - /// The display name, indexed in component order when duplicates exist; otherwise, if is or destroyed. - public static string GetScriptNameWithIndex(this Component targetComponent) + /// The display name, indexed in component order when duplicates exist; otherwise, if is or destroyed. + public static string GetDisplayNameWithIndex(this Component targetComponent) { - if (!targetComponent) return null; + if (!targetComponent) return string.Empty; var type = targetComponent.GetType(); - var components = targetComponent.GetComponents(type) - .Where(component => component.GetType() == type) - .ToArray(); + var displayName = targetComponent.GetDisplayName(); + using var pooled = ListPool.Get(out var components); + targetComponent.GetComponents(type, components); - if (components.Length <= 1) - return targetComponent.GetScriptName(); + var count = 0; + var index = 0; - for (var i = 0; i < components.Length; i++) + foreach (var component in components) { - if (components[i] == targetComponent) - return $"{targetComponent.GetScriptName()} ({i + 1})"; + if (!component || component.GetType() != type) continue; + + count++; + if (component == targetComponent) + index = count; } - return targetComponent.GetScriptName(); + return count > 1 && index > 0 + ? $"{displayName} ({index})" + : displayName; } } } diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Editor/Scripts/SerializedProperties/SerializePropertyExtensions.SetValue.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Editor/Scripts/SerializedProperties/SerializePropertyExtensions.SetValue.cs index 915c6c10..c0682fdf 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Editor/Scripts/SerializedProperties/SerializePropertyExtensions.SetValue.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Editor/Scripts/SerializedProperties/SerializePropertyExtensions.SetValue.cs @@ -21,6 +21,13 @@ public static T SetValueAndApply(this T property, int value) return property.SetIntAndApply(value); } + /// + public static T SetValueAndApplyWithoutUndo(this T property, int value) + where T : SerializedProperty + { + return property.SetIntAndApplyWithoutUndo(value); + } + /// /// Sets and returns the property for chaining. /// @@ -47,6 +54,19 @@ public static T SetIntAndApply(this T property, int value) { return property.SetInt(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Value to assign. + /// The same instance. + public static T SetIntAndApplyWithoutUndo(this T property, int value) + where T : SerializedProperty + { + return property.SetInt(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region Uint @@ -64,6 +84,13 @@ public static T SetValueAndApply(this T property, uint value) return property.SetUintAndApply(value); } + /// + public static T SetValueAndApplyWithoutUndo(this T property, uint value) + where T : SerializedProperty + { + return property.SetUintAndApplyWithoutUndo(value); + } + /// /// Sets and returns the property for chaining. /// @@ -90,6 +117,19 @@ public static T SetUintAndApply(this T property, uint value) { return property.SetUint(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Value to assign. + /// The same instance. + public static T SetUintAndApplyWithoutUndo(this T property, uint value) + where T : SerializedProperty + { + return property.SetUint(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region Long @@ -107,6 +147,13 @@ public static T SetValueAndApply(this T property, long value) return property.SetLongAndApply(value); } + /// + public static T SetValueAndApplyWithoutUndo(this T property, long value) + where T : SerializedProperty + { + return property.SetLongAndApplyWithoutUndo(value); + } + /// /// Sets and returns the property for chaining. /// @@ -133,6 +180,19 @@ public static T SetLongAndApply(this T property, long value) { return property.SetLong(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Value to assign. + /// The same instance. + public static T SetLongAndApplyWithoutUndo(this T property, long value) + where T : SerializedProperty + { + return property.SetLong(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region Ulong @@ -150,6 +210,13 @@ public static T SetValueAndApply(this T property, ulong value) return property.SetUlongAndApply(value); } + /// + public static T SetValueAndApplyWithoutUndo(this T property, ulong value) + where T : SerializedProperty + { + return property.SetUlongAndApplyWithoutUndo(value); + } + /// /// Sets and returns the property for chaining. /// @@ -176,6 +243,19 @@ public static T SetUlongAndApply(this T property, ulong value) { return property.SetUlong(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Value to assign. + /// The same instance. + public static T SetUlongAndApplyWithoutUndo(this T property, ulong value) + where T : SerializedProperty + { + return property.SetUlong(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region Float @@ -193,6 +273,13 @@ public static T SetValueAndApply(this T property, float value) return property.SetFloatAndApply(value); } + /// + public static T SetValueAndApplyWithoutUndo(this T property, float value) + where T : SerializedProperty + { + return property.SetFloatAndApplyWithoutUndo(value); + } + /// /// Sets and returns the property for chaining. /// @@ -219,6 +306,19 @@ public static T SetFloatAndApply(this T property, float value) { return property.SetFloat(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Value to assign. + /// The same instance. + public static T SetFloatAndApplyWithoutUndo(this T property, float value) + where T : SerializedProperty + { + return property.SetFloat(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region Double @@ -236,6 +336,13 @@ public static T SetValueAndApply(this T property, double value) return property.SetDoubleAndApply(value); } + /// + public static T SetValueAndApplyWithoutUndo(this T property, double value) + where T : SerializedProperty + { + return property.SetDoubleAndApplyWithoutUndo(value); + } + /// /// Sets and returns the property for chaining. /// @@ -262,6 +369,19 @@ public static T SetDoubleAndApply(this T property, double value) { return property.SetDouble(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Value to assign. + /// The same instance. + public static T SetDoubleAndApplyWithoutUndo(this T property, double value) + where T : SerializedProperty + { + return property.SetDouble(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region EnumIndex @@ -296,6 +416,19 @@ public static T SetEnumFlagAndApply(this T property, int value) return property.SetEnumFlag(value).ApplyModifiedProperties(); } + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Flag value to assign. + /// The same instance. + public static T SetEnumFlagAndApplyWithoutUndo(this T property, int value) + where T : SerializedProperty + { + return property.SetEnumFlag(value).ApplyModifiedPropertiesWithoutUndo(); + } + /// /// Sets and returns the property for chaining. /// @@ -326,6 +459,19 @@ public static T SetEnumIndexAndApply(this T property, int value) { return property.SetEnumIndex(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Index value to assign. + /// The same instance. + public static T SetEnumIndexAndApplyWithoutUndo(this T property, int value) + where T : SerializedProperty + { + return property.SetEnumIndex(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region Bool @@ -343,6 +489,13 @@ public static T SetValueAndApply(this T property, bool value) return property.SetBoolAndApply(value); } + /// + public static T SetValueAndApplyWithoutUndo(this T property, bool value) + where T : SerializedProperty + { + return property.SetBoolAndApplyWithoutUndo(value); + } + /// /// Sets and returns the property for chaining. /// @@ -369,6 +522,19 @@ public static T SetBoolAndApply(this T property, bool value) { return property.SetBool(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Value to assign. + /// The same instance. + public static T SetBoolAndApplyWithoutUndo(this T property, bool value) + where T : SerializedProperty + { + return property.SetBool(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region Rect @@ -386,6 +552,13 @@ public static T SetValueAndApply(this T property, Rect value) return property.SetRectAndApply(value); } + /// + public static T SetValueAndApplyWithoutUndo(this T property, Rect value) + where T : SerializedProperty + { + return property.SetRectAndApplyWithoutUndo(value); + } + /// /// Sets and returns the property for chaining. /// @@ -412,6 +585,19 @@ public static T SetRectAndApply(this T property, Rect value) { return property.SetRect(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Value to assign. + /// The same instance. + public static T SetRectAndApplyWithoutUndo(this T property, Rect value) + where T : SerializedProperty + { + return property.SetRect(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region RectInt @@ -429,6 +615,13 @@ public static T SetValueAndApply(this T property, RectInt value) return property.SetRectIntAndApply(value); } + /// + public static T SetValueAndApplyWithoutUndo(this T property, RectInt value) + where T : SerializedProperty + { + return property.SetRectIntAndApplyWithoutUndo(value); + } + /// /// Sets and returns the property for chaining. /// @@ -455,6 +648,19 @@ public static T SetRectIntAndApply(this T property, RectInt value) { return property.SetRectInt(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Value to assign. + /// The same instance. + public static T SetRectIntAndApplyWithoutUndo(this T property, RectInt value) + where T : SerializedProperty + { + return property.SetRectInt(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region Bounds @@ -472,6 +678,13 @@ public static T SetValueAndApply(this T property, Bounds value) return property.SetBoundsAndApply(value); } + /// + public static T SetValueAndApplyWithoutUndo(this T property, Bounds value) + where T : SerializedProperty + { + return property.SetBoundsAndApplyWithoutUndo(value); + } + /// /// Sets and returns the property for chaining. /// @@ -498,6 +711,19 @@ public static T SetBoundsAndApply(this T property, Bounds value) { return property.SetBounds(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Value to assign. + /// The same instance. + public static T SetBoundsAndApplyWithoutUndo(this T property, Bounds value) + where T : SerializedProperty + { + return property.SetBounds(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region BoundsInt @@ -515,6 +741,13 @@ public static T SetValueAndApply(this T property, BoundsInt value) return property.SetBoundsIntAndApply(value); } + /// + public static T SetValueAndApplyWithoutUndo(this T property, BoundsInt value) + where T : SerializedProperty + { + return property.SetBoundsIntAndApplyWithoutUndo(value); + } + /// /// Sets and returns the property for chaining. /// @@ -541,6 +774,19 @@ public static T SetBoundsIntAndApply(this T property, BoundsInt value) { return property.SetBoundsInt(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Value to assign. + /// The same instance. + public static T SetBoundsIntAndApplyWithoutUndo(this T property, BoundsInt value) + where T : SerializedProperty + { + return property.SetBoundsInt(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region Color @@ -558,6 +804,13 @@ public static T SetValueAndApply(this T property, Color value) return property.SetColorAndApply(value); } + /// + public static T SetValueAndApplyWithoutUndo(this T property, Color value) + where T : SerializedProperty + { + return property.SetColorAndApplyWithoutUndo(value); + } + /// /// Sets and returns the property for chaining. /// @@ -584,6 +837,19 @@ public static T SetColorAndApply(this T property, Color value) { return property.SetColor(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Value to assign. + /// The same instance. + public static T SetColorAndApplyWithoutUndo(this T property, Color value) + where T : SerializedProperty + { + return property.SetColor(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region Gradient @@ -601,6 +867,13 @@ public static T SetValueAndApply(this T property, Gradient value) return property.SetGradientAndApply(value); } + /// + public static T SetValueAndApplyWithoutUndo(this T property, Gradient value) + where T : SerializedProperty + { + return property.SetGradientAndApplyWithoutUndo(value); + } + /// /// Sets and returns the property for chaining. /// @@ -627,6 +900,19 @@ public static T SetGradientAndApply(this T property, Gradient value) { return property.SetGradient(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Value to assign. + /// The same instance. + public static T SetGradientAndApplyWithoutUndo(this T property, Gradient value) + where T : SerializedProperty + { + return property.SetGradient(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region Hash128 @@ -644,6 +930,13 @@ public static T SetValueAndApply(this T property, Hash128 value) return property.SetHash128AndApply(value); } + /// + public static T SetValueAndApplyWithoutUndo(this T property, Hash128 value) + where T : SerializedProperty + { + return property.SetHash128AndApplyWithoutUndo(value); + } + /// /// Sets and returns the property for chaining. /// @@ -670,6 +963,19 @@ public static T SetHash128AndApply(this T property, Hash128 value) { return property.SetHash128(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Value to assign. + /// The same instance. + public static T SetHash128AndApplyWithoutUndo(this T property, Hash128 value) + where T : SerializedProperty + { + return property.SetHash128(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region Vector4 @@ -687,6 +993,13 @@ public static T SetValueAndApply(this T property, Vector4 value) return property.SetVector4AndApply(value); } + /// + public static T SetValueAndApplyWithoutUndo(this T property, Vector4 value) + where T : SerializedProperty + { + return property.SetVector4AndApplyWithoutUndo(value); + } + /// /// Sets and returns the property for chaining. /// @@ -713,6 +1026,19 @@ public static T SetVector4AndApply(this T property, Vector4 value) { return property.SetVector4(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Value to assign. + /// The same instance. + public static T SetVector4AndApplyWithoutUndo(this T property, Vector4 value) + where T : SerializedProperty + { + return property.SetVector4(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region Vector3 @@ -730,6 +1056,13 @@ public static T SetValueAndApply(this T property, Vector3 value) return property.SetVector3AndApply(value); } + /// + public static T SetValueAndApplyWithoutUndo(this T property, Vector3 value) + where T : SerializedProperty + { + return property.SetVector3AndApplyWithoutUndo(value); + } + /// /// Sets and returns the property for chaining. /// @@ -756,6 +1089,19 @@ public static T SetVector3AndApply(this T property, Vector3 value) { return property.SetVector3(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Value to assign. + /// The same instance. + public static T SetVector3AndApplyWithoutUndo(this T property, Vector3 value) + where T : SerializedProperty + { + return property.SetVector3(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region Vector3Int @@ -773,6 +1119,13 @@ public static T SetValueAndApply(this T property, Vector3Int value) return property.SetVector3IntAndApply(value); } + /// + public static T SetValueAndApplyWithoutUndo(this T property, Vector3Int value) + where T : SerializedProperty + { + return property.SetVector3IntAndApplyWithoutUndo(value); + } + /// /// Sets and returns the property for chaining. /// @@ -799,6 +1152,19 @@ public static T SetVector3IntAndApply(this T property, Vector3Int value) { return property.SetVector3Int(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Value to assign. + /// The same instance. + public static T SetVector3IntAndApplyWithoutUndo(this T property, Vector3Int value) + where T : SerializedProperty + { + return property.SetVector3Int(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region Vector2 @@ -816,6 +1182,13 @@ public static T SetValueAndApply(this T property, Vector2 value) return property.SetVector2AndApply(value); } + /// + public static T SetValueAndApplyWithoutUndo(this T property, Vector2 value) + where T : SerializedProperty + { + return property.SetVector2AndApplyWithoutUndo(value); + } + /// /// Sets and returns the property for chaining. /// @@ -842,6 +1215,19 @@ public static T SetVector2AndApply(this T property, Vector2 value) { return property.SetVector2(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Value to assign. + /// The same instance. + public static T SetVector2AndApplyWithoutUndo(this T property, Vector2 value) + where T : SerializedProperty + { + return property.SetVector2(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region Vector2Int @@ -859,6 +1245,13 @@ public static T SetValueAndApply(this T property, Vector2Int value) return property.SetVector2IntAndApply(value); } + /// + public static T SetValueAndApplyWithoutUndo(this T property, Vector2Int value) + where T : SerializedProperty + { + return property.SetVector2IntAndApplyWithoutUndo(value); + } + /// /// Sets and returns the property for chaining. /// @@ -885,6 +1278,19 @@ public static T SetVector2IntAndApply(this T property, Vector2Int value) { return property.SetVector2Int(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Value to assign. + /// The same instance. + public static T SetVector2IntAndApplyWithoutUndo(this T property, Vector2Int value) + where T : SerializedProperty + { + return property.SetVector2Int(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region Quaternion @@ -902,6 +1308,13 @@ public static T SetValueAndApply(this T property, Quaternion value) return property.SetQuaternionAndApply(value); } + /// + public static T SetValueAndApplyWithoutUndo(this T property, Quaternion value) + where T : SerializedProperty + { + return property.SetQuaternionAndApplyWithoutUndo(value); + } + /// /// Sets and returns the property for chaining. /// @@ -928,6 +1341,19 @@ public static T SetQuaternionAndApply(this T property, Quaternion value) { return property.SetQuaternion(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Value to assign. + /// The same instance. + public static T SetQuaternionAndApplyWithoutUndo(this T property, Quaternion value) + where T : SerializedProperty + { + return property.SetQuaternion(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region String @@ -945,6 +1371,13 @@ public static T SetValueAndApply(this T property, string value) return property.SetStringAndApply(value); } + /// + public static T SetValueAndApplyWithoutUndo(this T property, string value) + where T : SerializedProperty + { + return property.SetStringAndApplyWithoutUndo(value); + } + /// /// Sets and returns the property for chaining. /// @@ -971,6 +1404,19 @@ public static T SetStringAndApply(this T property, string value) { return property.SetString(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Value to assign. + /// The same instance. + public static T SetStringAndApplyWithoutUndo(this T property, string value) + where T : SerializedProperty + { + return property.SetString(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region AnimationCurve @@ -988,6 +1434,13 @@ public static T SetValueAndApply(this T property, AnimationCurve value) return property.SetAnimationCurveAndApply(value); } + /// + public static T SetValueAndApplyWithoutUndo(this T property, AnimationCurve value) + where T : SerializedProperty + { + return property.SetAnimationCurveAndApplyWithoutUndo(value); + } + /// /// Sets and returns the property for chaining. /// @@ -1014,6 +1467,19 @@ public static T SetAnimationCurveAndApply(this T property, AnimationCurve val { return property.SetAnimationCurve(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Value to assign. + /// The same instance. + public static T SetAnimationCurveAndApplyWithoutUndo(this T property, AnimationCurve value) + where T : SerializedProperty + { + return property.SetAnimationCurve(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region ArraySize @@ -1044,6 +1510,19 @@ public static T SetArraySizeAndApply(this T property, int size) return property.SetArraySize(size).ApplyModifiedProperties(); } + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target array property. + /// New array size. + /// The same instance. + public static T SetArraySizeAndApplyWithoutUndo(this T property, int size) + where T : SerializedProperty + { + return property.SetArraySize(size).ApplyModifiedPropertiesWithoutUndo(); + } + /// /// Increases by and returns the property for chaining. /// @@ -1070,6 +1549,19 @@ public static T AddArraySizeAndApply(this T property, int value = 1) return SetArraySizeAndApply(property, size: property.arraySize + value); } + /// + /// Increases by then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target array property. + /// Amount to add to the current array size. + /// The same instance. + public static T AddArraySizeAndApplyWithoutUndo(this T property, int value = 1) + where T : SerializedProperty + { + return SetArraySizeAndApplyWithoutUndo(property, size: property.arraySize + value); + } + /// /// Decreases by and returns the property for chaining. /// @@ -1095,6 +1587,19 @@ public static T RemoveArraySizeAndApply(this T property, int value = 1) { return SetArraySizeAndApply(property, size: property.arraySize - value); } + + /// + /// Decreases by then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target array property. + /// Amount to subtract from the current array size. + /// The same instance. + public static T RemoveArraySizeAndApplyWithoutUndo(this T property, int value = 1) + where T : SerializedProperty + { + return SetArraySizeAndApplyWithoutUndo(property, size: property.arraySize - value); + } #endregion #region ManagedReference @@ -1124,6 +1629,19 @@ public static T SetManagedReferenceAndApply(this T property, object value) { return property.SetManagedReference(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property (must be a [SerializeReference] field). + /// Managed reference value to assign. + /// The same instance. + public static T SetManagedReferenceAndApplyWithoutUndo(this T property, object value) + where T : SerializedProperty + { + return property.SetManagedReference(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region ObjectReference @@ -1153,6 +1671,19 @@ public static T SetObjectReferenceAndApply(this T property, Object value) { return property.SetObjectReference(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// reference to assign. + /// The same instance. + public static T SetObjectReferenceAndApplyWithoutUndo(this T property, Object value) + where T : SerializedProperty + { + return property.SetObjectReference(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region ExposedReference @@ -1182,6 +1713,19 @@ public static T SetExposedReferenceAndApply(this T property, Object value) { return property.SetExposedReference(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// exposed reference to assign. + /// The same instance. + public static T SetExposedReferenceAndApplyWithoutUndo(this T property, Object value) + where T : SerializedProperty + { + return property.SetExposedReference(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #region Boxed @@ -1211,6 +1755,19 @@ public static T SetBoxedAndApply(this T property, object value) { return property.SetBoxed(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Boxed value to assign. + /// The same instance. + public static T SetBoxedAndApplyWithoutUndo(this T property, object value) + where T : SerializedProperty + { + return property.SetBoxed(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #if UNITY_6000_2_OR_NEWER @@ -1229,6 +1786,13 @@ public static T SetValueAndApply(this T property, EntityId value) return property.SetEntityIdAndApply(value); } + /// + public static T SetValueAndApplyWithoutUndo(this T property, EntityId value) + where T : SerializedProperty + { + return property.SetEntityIdAndApplyWithoutUndo(value); + } + /// /// Sets and returns the property for chaining. /// @@ -1255,6 +1819,19 @@ public static T SetEntityIdAndApply(this T property, EntityId value) { return property.SetEntityId(value).ApplyModifiedProperties(); } + + /// + /// Sets then applies modified properties without recording Undo. + /// + /// Concrete type. + /// Target property. + /// Value to assign. + /// The same instance. + public static T SetEntityIdAndApplyWithoutUndo(this T property, EntityId value) + where T : SerializedProperty + { + return property.SetEntityId(value).ApplyModifiedPropertiesWithoutUndo(); + } #endregion #endif } diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Editor/Scripts/VisualElements/Internal/Components/AspidInspectorHeaders/AspidInspectorHeader.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Editor/Scripts/VisualElements/Internal/Components/AspidInspectorHeaders/AspidInspectorHeader.cs index 050de039..aba81bc3 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Editor/Scripts/VisualElements/Internal/Components/AspidInspectorHeaders/AspidInspectorHeader.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Editor/Scripts/VisualElements/Internal/Components/AspidInspectorHeaders/AspidInspectorHeader.cs @@ -68,10 +68,10 @@ public AspidInspectorHeader(AspidInspectorHeaderPreset preset) : this(preset, obj: null) { } public AspidInspectorHeader(Object obj) - : this(AspidInspectorHeaderPreset.Default.SetText(obj.GetScriptName()), obj) { } + : this(AspidInspectorHeaderPreset.Default.SetText(obj.GetDisplayName()), obj) { } public AspidInspectorHeader(Component component) - : this(AspidInspectorHeaderPreset.Default.SetText(component.GetScriptNameWithIndex()), component) { } + : this(AspidInspectorHeaderPreset.Default.SetText(component.GetDisplayNameWithIndex()), component) { } public AspidInspectorHeader(string label, Object obj) : this(AspidInspectorHeaderPreset.Default.SetText(label), obj) { } diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Editor/Scripts/Windows/SampleThemeWindow.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Editor/Scripts/Windows/SampleThemeWindow.cs new file mode 100644 index 00000000..d2698325 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Editor/Scripts/Windows/SampleThemeWindow.cs @@ -0,0 +1,357 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEditor.UIElements; +using UnityEngine; +using UnityEngine.SceneManagement; +using UnityEngine.Rendering; +using UnityEngine.UIElements; + +namespace Aspid.FastTools.Editors.Internal +{ + internal enum SampleThemeMode { Authored, Dark, Light } + + [Serializable] + internal sealed class SampleThemePalette + { + public Color Background; + public Color Platform; + public Color Edge; + public Color Porcelain; + public Color Accent; + public Color Text; + public Color MutedText; + public Color Cyan; + public Color Grid; + [Range(0, 1), Tooltip("Blend animated character and flock colors toward white in the light preview.")] + public float AnimatedColorLift = 0.45f; + + internal static SampleThemePalette Dark() => new() + { + Background = Hex("0E1015"), Platform = new Color(0.085f, 0.13f, 0.17f), + Edge = new Color(0.16f, 0.24f, 0.29f), Porcelain = new Color(0.82f, 0.89f, 0.91f), + Accent = new Color(0.65f, 0.95f, 0.4f), Text = Hex("F0F0F2"), MutedText = Hex("92959F"), + Cyan = Hex("40D9FF"), Grid = Hex("293D4A"), + }; + + internal static SampleThemePalette Light() => new() + { + Background = Hex("EEF0F3"), Platform = Hex("D3D5DB"), Edge = Hex("B4B7C0"), + Porcelain = Hex("CFD9DF"), Accent = Hex("A6D8A8"), Text = Hex("2A2C31"), MutedText = Hex("5F636B"), + Cyan = Hex("97C7D8"), Grid = Hex("B4BDC8"), + }; + + internal static Color Hex(string value) + { + ColorUtility.TryParseHtmlString("#" + value, out var color); + return color; + } + } + + [Serializable] + internal sealed class SampleSurfaceColors + { + public Color Grass; + public Color Stone; + public Color Metal; + public Color Water; + public Color Sand; + + internal Color Get(string surface, Color fallback) => surface switch + { + "Grass" => Grass, "Stone" => Stone, "Metal" => Metal, + "Water" => Water, "Sand" => Sand, _ => fallback, + }; + } + + [Serializable] + internal sealed class SampleSurfaceTheme + { + public Color Caption = SampleThemePalette.Hex("455363"); + public SampleSurfaceColors Tiles = new() + { + Grass = SampleThemePalette.Hex("9ABD9F"), Stone = SampleThemePalette.Hex("BBC0CB"), + Metal = SampleThemePalette.Hex("93A9B5"), Water = SampleThemePalette.Hex("91BFD5"), + Sand = SampleThemePalette.Hex("DBCAA5"), + }; + public SampleSurfaceColors Trails = new() + { + Grass = SampleThemePalette.Hex("78B795"), Stone = SampleThemePalette.Hex("B29BD4"), + Metal = SampleThemePalette.Hex("D998A8"), Water = SampleThemePalette.Hex("61B4CE"), + Sand = SampleThemePalette.Hex("D4A662"), + }; + } + + [FilePath("ProjectSettings/AspidFastToolsSampleThemes.asset", FilePathAttribute.Location.ProjectFolder)] + internal sealed class SampleThemeSettings : ScriptableSingleton + { + [Tooltip("Shared palette for dark sample recordings.")] + [SerializeField] private SampleThemePalette _dark = SampleThemePalette.Dark(); + [Tooltip("Shared palette for light sample recordings.")] + [SerializeField] private SampleThemePalette _light = SampleThemePalette.Light(); + [Tooltip("Light theme colors for EnumValues tiles, trails and their captions.")] + [SerializeField] private SampleSurfaceTheme _enumValuesLight = new(); + internal SampleSurfaceTheme EnumValuesLight => _enumValuesLight; + internal SampleThemePalette Get(SampleThemeMode mode) => mode == SampleThemeMode.Light ? _light : _dark; + internal void Persist() => Save(true); + } + + [InitializeOnLoad] + internal static class SampleThemePreview + { + private const string ModeKey = "Aspid.FastTools.SampleTheme"; + private static readonly Dictionary Cameras = new(); + private static readonly Dictionary Labels = new(); + private static readonly Dictionary Renderers = new(); + private static readonly Dictionary SurfaceReferences = new(); + private static readonly Dictionary SurfacePalettes = new(); + private static readonly Dictionary AnimatedRenderers = new(); + private static double _nextUpdate; + private static bool _suspended; + + internal static SampleThemeMode Mode => (SampleThemeMode)SessionState.GetInt(ModeKey, 0); + + static SampleThemePreview() + { + EditorApplication.update += Update; + Camera.onPreCull += BeforeCamera; + Camera.onPostRender += _ => RestoreAnimated(); + RenderPipelineManager.beginCameraRendering += (_, camera) => BeforeCamera(camera); + RenderPipelineManager.endCameraRendering += (_, _) => RestoreAnimated(); + AssemblyReloadEvents.beforeAssemblyReload += Restore; + EditorApplication.quitting += Restore; + EditorApplication.playModeStateChanged += state => + { + _suspended = state == PlayModeStateChange.ExitingEditMode || state == PlayModeStateChange.ExitingPlayMode; + Restore(); + }; + EditorSceneManager.sceneClosing += (_, _) => Restore(); + EditorSceneManager.sceneSaving += (_, _) => + { + _suspended = true; + Restore(); + EditorApplication.delayCall += () => _suspended = false; + }; + } + + internal static void SetMode(SampleThemeMode mode) + { + Restore(); + SessionState.SetInt(ModeKey, (int)mode); + Apply(); + } + + internal static void Refresh() + { + Restore(); + Apply(); + } + + private static void Update() + { + if (EditorApplication.timeSinceStartup < _nextUpdate) return; + _nextUpdate = EditorApplication.timeSinceStartup + 0.3; + Apply(); + } + + internal static void Apply() + { + if (_suspended || Mode == SampleThemeMode.Authored) return; + var palette = SampleThemeSettings.instance.Get(Mode); + // Only shipped sample scenes with their framing component participate. + var scenes = Resources.FindObjectsOfTypeAll() + .Where(component => component != null && component.GetType().Name == "SampleFrame" + && component.GetType().Namespace?.StartsWith("Aspid.FastTools.Samples.", StringComparison.Ordinal) == true + && component.gameObject.scene.IsValid() && component.gameObject.scene.isLoaded) + .Select(component => component.gameObject.scene).Distinct(); + foreach (var scene in scenes) + ApplyScene(scene, palette); + SceneView.RepaintAll(); + } + + private static void ApplyScene(Scene scene, SampleThemePalette palette) + { + foreach (var root in scene.GetRootGameObjects()) + { + if (Mode == SampleThemeMode.Light) + foreach (var component in root.GetComponentsInChildren(true)) + ApplySurfacePalette(component); + foreach (var camera in root.GetComponentsInChildren(true)) + { + if (Cameras.ContainsKey(camera)) continue; + Cameras.Add(camera, camera.backgroundColor); + camera.backgroundColor = palette.Background; + } + foreach (var text in root.GetComponentsInChildren(true)) + { + if (Labels.ContainsKey(text)) continue; + if (text.name == "Surface label" && Mode != SampleThemeMode.Light) continue; + Labels.Add(text, text.color); + var color = text.name == "Surface label" ? SampleThemeSettings.instance.EnumValuesLight.Caption + : text.color.maxColorComponent > 0.8f ? palette.Text : palette.MutedText; + // Legacy TextMesh sends vertex colors directly to the font shader. + text.color = Mode == SampleThemeMode.Light && QualitySettings.activeColorSpace == ColorSpace.Linear + ? color.linear : color; + } + foreach (var renderer in root.GetComponentsInChildren(true)) + { + if (Renderers.ContainsKey(renderer) || (renderer.HasPropertyBlock() && renderer is not LineRenderer)) continue; + var material = renderer.sharedMaterial; + if (material == null || !AssetDatabase.GetAssetPath(material).Contains("/Presentation/")) continue; + Color color; + switch (material.name) + { + case "Graphite": color = palette.Platform; break; + case "Edge": color = palette.Edge; break; + case "Porcelain": color = palette.Porcelain; break; + case "Venom": color = palette.Accent; break; + case "Cyan" when Mode == SampleThemeMode.Light: color = palette.Cyan; break; + case "Grid" when Mode == SampleThemeMode.Light: color = palette.Grid; break; + default: continue; + } + var original = new MaterialPropertyBlock(); + renderer.GetPropertyBlock(original); + Renderers.Add(renderer, original); + var block = new MaterialPropertyBlock(); + renderer.GetPropertyBlock(block); + block.SetColor("_Color", color); + block.SetColor("_BaseColor", color); + renderer.SetPropertyBlock(block); + } + } + } + + private static void BeforeCamera(Camera camera) + { + RestoreAnimated(); + if (_suspended || Mode != SampleThemeMode.Light || camera == null) return; + if (!camera.GetComponents().Any(component => component != null + && component.GetType().Name == "SampleFrame" + && component.GetType().Namespace?.StartsWith("Aspid.FastTools.Samples.", StringComparison.Ordinal) == true)) return; + var palette = SampleThemeSettings.instance.Get(Mode); + foreach (var root in camera.gameObject.scene.GetRootGameObjects()) + foreach (var renderer in root.GetComponentsInChildren(true)) + { + if (!renderer.HasPropertyBlock()) continue; + var material = renderer.sharedMaterial; + var path = material == null ? "" : AssetDatabase.GetAssetPath(material); + if (!path.Contains("/Presentation/") || path.Contains("/EnumValues/")) continue; + if (Renderers.ContainsKey(renderer)) continue; + var original = new MaterialPropertyBlock(); + renderer.GetPropertyBlock(original); + var color = original.GetColor("_Color"); + if (color.a <= 0) continue; + var themed = new MaterialPropertyBlock(); + renderer.GetPropertyBlock(themed); + color = Color.Lerp(color, Color.white, palette.AnimatedColorLift); + themed.SetColor("_Color", color); + themed.SetColor("_BaseColor", color); + AnimatedRenderers.Add(renderer, original); + renderer.SetPropertyBlock(themed); + } + } + + private static void RestoreAnimated() + { + foreach (var pair in AnimatedRenderers) + if (pair.Key != null) pair.Key.SetPropertyBlock(pair.Value); + AnimatedRenderers.Clear(); + } + + private static void ApplySurfacePalette(MonoBehaviour component) + { + if (component == null || SurfaceReferences.ContainsKey(component)) return; + var type = component.GetType(); + if (type.Namespace != "Aspid.FastTools.Samples.EnumValues" + || (type.Name != "SurfaceTile" && type.Name != "Walker")) return; + // Samples are optional assemblies. Reflect only their known palette field, keeping + // the package editor independent of whether the sample has been imported. + var field = type.GetField("_palette", BindingFlags.Instance | BindingFlags.NonPublic); + if (field?.GetValue(component) is not ScriptableObject original) return; + if (!SurfacePalettes.TryGetValue(original, out var preview)) + { + preview = UnityEngine.Object.Instantiate(original); + preview.hideFlags = HideFlags.HideAndDontSave; + var serialized = new SerializedObject(preview); + var theme = SampleThemeSettings.instance.EnumValuesLight; + ApplyTable(serialized.FindProperty("_tileColors._values"), theme.Tiles); + ApplyTable(serialized.FindProperty("_footprintColors._values"), theme.Trails); + serialized.ApplyModifiedPropertiesWithoutUndo(); + SurfacePalettes.Add(original, preview); + } + SurfaceReferences.Add(component, (field, original)); + field.SetValue(component, preview); + RefreshTile(component); + } + + private static void ApplyTable(SerializedProperty rows, SampleSurfaceColors colors) + { + if (rows == null || !rows.isArray) return; + for (var i = 0; i < rows.arraySize; i++) + { + var row = rows.GetArrayElementAtIndex(i); + var value = row.FindPropertyRelative("_value"); + value.colorValue = colors.Get(row.FindPropertyRelative("_key").stringValue, value.colorValue); + } + } + + private static void RefreshTile(MonoBehaviour component) => component.GetType() + .GetMethod("Refresh", BindingFlags.Instance | BindingFlags.NonPublic)?.Invoke(component, null); + + internal static void Restore() + { + RestoreAnimated(); + foreach (var pair in SurfaceReferences) + { + if (pair.Key == null) continue; + pair.Value.Field.SetValue(pair.Key, pair.Value.Original); + RefreshTile(pair.Key); + } + foreach (var preview in SurfacePalettes.Values) + if (preview != null) UnityEngine.Object.DestroyImmediate(preview); + SurfaceReferences.Clear(); SurfacePalettes.Clear(); + foreach (var pair in Cameras) if (pair.Key != null) pair.Key.backgroundColor = pair.Value; + foreach (var pair in Labels) if (pair.Key != null) pair.Key.color = pair.Value; + foreach (var pair in Renderers) if (pair.Key != null) pair.Key.SetPropertyBlock(pair.Value); + Cameras.Clear(); Labels.Clear(); Renderers.Clear(); + SceneView.RepaintAll(); + } + } + + internal sealed class SampleThemeWindow : EditorWindow + { + [MenuItem("Tools/Aspid 🐍/FastTools/Sample Themes", priority = 41)] + private static void Open() => GetWindow("Sample Themes"); + + private void CreateGUI() + { + var root = rootVisualElement; + root.style.paddingLeft = root.style.paddingRight = 12; + root.style.paddingTop = root.style.paddingBottom = 12; + minSize = new Vector2(360, 360); + var mode = new EnumField("Preview", SampleThemePreview.Mode); + mode.RegisterValueChangedCallback(evt => SampleThemePreview.SetMode((SampleThemeMode)evt.newValue)); + root.Add(mode); + root.Add(new HelpBox("Applies to open EnumValues, Types, SerializeReferences and ProfilerMarkers scenes, including Play Mode. Authored restores the original look. Scene and material assets keep their authored colors.", HelpBoxMessageType.Info)); + var scroll = new ScrollView(); + scroll.style.flexGrow = 1; + root.Add(scroll); + var settings = new SerializedObject(SampleThemeSettings.instance); + scroll.Add(new PropertyField(settings.FindProperty("_light"), "Light palette")); + scroll.Add(new PropertyField(settings.FindProperty("_dark"), "Dark palette")); + scroll.Add(new PropertyField(settings.FindProperty("_enumValuesLight"), "EnumValues light surfaces")); + scroll.Bind(settings); + scroll.RegisterCallback(_ => + { + SampleThemeSettings.instance.Persist(); + SampleThemePreview.Refresh(); + }); + var hint = new Label("Palettes are shared by all four scenes and saved in ProjectSettings/AspidFastToolsSampleThemes.asset."); + hint.style.whiteSpace = WhiteSpace.Normal; + root.Add(hint); + } + } +} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Editor/Scripts/Windows/SampleThemeWindow.cs.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Editor/Scripts/Windows/SampleThemeWindow.cs.meta new file mode 100644 index 00000000..fd1ed34b --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Editor/Scripts/Windows/SampleThemeWindow.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: cc49a5efe06f43ff89b792fbb6c647cc diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation.meta new file mode 100644 index 00000000..137289bf --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 974d2623324046168fa833078753a5b1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images.meta new file mode 100644 index 00000000..9fb94a7d --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 97c97b35069c4a8a85740357b0cf7110 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images/ability-catalog-light.png b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images/ability-catalog-light.png new file mode 100644 index 00000000..e0ab1892 Binary files /dev/null and b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images/ability-catalog-light.png differ diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images/ability-catalog-light.png.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images/ability-catalog-light.png.meta new file mode 100644 index 00000000..53b77568 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images/ability-catalog-light.png.meta @@ -0,0 +1,143 @@ +fileFormatVersion: 2 +guid: f54d208f78744734a1488d8746ab8b20 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: iOS + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images/ability-catalog.png b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images/ability-catalog.png new file mode 100644 index 00000000..0db51668 Binary files /dev/null and b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images/ability-catalog.png differ diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images/ability-catalog.png.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images/ability-catalog.png.meta new file mode 100644 index 00000000..a69d3a56 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images/ability-catalog.png.meta @@ -0,0 +1,143 @@ +fileFormatVersion: 2 +guid: b6c3d083ec66419092c48a3b878cf5ab +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: iOS + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images/demo-light.gif b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images/demo-light.gif new file mode 100644 index 00000000..919bc27c Binary files /dev/null and b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images/demo-light.gif differ diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images/demo-light.gif.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images/demo-light.gif.meta new file mode 100644 index 00000000..f1f6b585 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images/demo-light.gif.meta @@ -0,0 +1,143 @@ +fileFormatVersion: 2 +guid: c909115be86143f7bc356c84a127a2dd +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: iOS + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images/demo.gif b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images/demo.gif new file mode 100644 index 00000000..f66a0533 Binary files /dev/null and b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images/demo.gif differ diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images/demo.gif.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images/demo.gif.meta new file mode 100644 index 00000000..50d099bb --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/Images/demo.gif.meta @@ -0,0 +1,143 @@ +fileFormatVersion: 2 +guid: 787429bbb8f84589a503b764273ddffc +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: iOS + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/README.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/README.md similarity index 70% rename from Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/README.md rename to Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/README.md index e4d1e08e..97e1a880 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/README.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/README.md @@ -1,6 +1,6 @@ # EditorTools Sample -An editor window and a custom inspector for a small `AbilityConfig` asset type, built entirely in code with the package's editor helpers: the fluent `VisualElement` extensions, `SerializedProperty` setters, display-name helpers and the type-picker window as a public API. The references live in [VisualElement Extensions](../../Documentation/07-visual-element-extensions.md), [SerializedProperty Extensions](../../Documentation/08-serialized-property-extensions.md), [Editor Helpers](../../Documentation/09-editor-helpers.md) and [TypeSelectorWindow](../../Documentation/02-serializable-types.md#typeselectorwindow). +An editor window and a custom inspector for a small `AbilityConfig` asset type, built entirely in code with the package's editor helpers: the fluent `VisualElement` extensions, `SerializedProperty` setters, display-name helpers and the type-picker window as a public API. The references live in [VisualElement Extensions](../../../Documentation/07-visual-element-extensions.md), [SerializedProperty Extensions](../../../Documentation/08-serialized-property-extensions.md), [Editor Helpers](../../../Documentation/09-editor-helpers.md) and [TypeSelectorWindow](../../../Documentation/02-serializable-types.md#typeselectorwindow). ```csharp rootVisualElement @@ -9,18 +9,26 @@ rootVisualElement .AddChild(details.SetFlexGrow(1)); ``` +Select an ability to edit its properties and view its effect description. + ## Open it 1. Import the sample. There is no scene. 2. Open **Tools → Aspid 🐍 → FastTools → Samples → Ability Catalog**. The left pane lists the four `AbilityConfig` assets from `Data/`; select one. +Use **Theme → Editor / Dark / Light** in the window header to choose the recording palette. **Editor** follows the Unity editor theme. + +![Halve cooldown, +5 MP updates both the fields and effect description; Undo restores them.](Images/demo.gif) + +Halve cooldown, +5 MP updates both the fields and effect description; Undo restores them. + ## Try 1. **A ListView in five calls.** `SetItemsSource`, `SetMakeItem`, `SetBindItem`, `SetFixedItemHeight`, `AddSelectionChanged`: the whole list is one chain. Type in the search field; `AddValueChanged` filters the source and `RefreshItems` redraws. 2. **Binding.** The detail pane's fields are plain `PropertyField`s under one container with `.BindTo(serializedObject)`. Edit the name: the list updates, the asset is dirty, Undo works, and the standard Inspector shows the same value. 3. **Typed property setters.** Press **Halve cooldown, +5 MP**. The handler chains `SetFloat` and `SetIntAndApply` on `SerializedProperty` instead of touching the asset directly, so the change is one Undo step and lands in the file. -4. **The type picker from code.** Press **Change…** next to `Effect`. `TypeSelectorWindow.Show` opens the same searchable window `[TypeSelector]` uses, anchored to the button and filtered to `IAbilityEffect` implementations; the result is written into a `string` property. Pick `HealEffect`. -5. **Display names and script access.** The pane title is `config.GetScriptName()`, which honors `[AddComponentMenu]` when present. Double-click it: `AddOpenScriptCommand` opens `AbilityConfig.cs` in your IDE. +4. **The type picker from code.** Press **Change…** next to `Effect`. `TypeSelectorWindow.Show` opens the same searchable window `[TypeSelector]` uses, anchored to the button and filtered to `IAbilityEffect` implementations; the result is written into a `string` property. Pick `HealEffect`. Change `Mana Cost` in the window or Inspector, then Undo/Redo: the effect description follows the data. +5. **Display names and script access.** The pane title is `config.GetDisplayName()`, which honors `[AddComponentMenu]` when present. Double-click it: `AddOpenScriptCommand` opens `AbilityConfig.cs` in your IDE. 6. **The inspector.** Select `Data/Sprint.asset` in the Project window. `AbilityConfigEditor` draws a card with a status badge and a warning `HelpBox` that appears only while `Mana Cost` is `0`; `PropertyField.AddValueChanged` drives both. Set the cost to `10` and back. 7. **Create.** Press **Create** to add an asset next to the selected one; it appears in the list, selected. @@ -28,7 +36,7 @@ rootVisualElement | File | Shows | |---|---| -| `Scripts/Editor/AbilityCatalogWindow.cs` | `ListView` extensions, `BindTo`, `SetFloat` / `SetIntAndApply`, `TypeSelectorWindow.Show` with a `TypeSelectorFilter`, `GetScriptName`, `AddOpenScriptCommand` | +| `Scripts/Editor/AbilityCatalogWindow.cs` | `ListView` extensions, `BindTo`, `SetFloat` / `SetIntAndApply`, `TypeSelectorWindow.Show` with a `TypeSelectorFilter`, `GetDisplayName`, `AddOpenScriptCommand` | | `Scripts/Editor/AbilityConfigEditor.cs` | A reactive custom inspector with the style and layout setters | | `Scripts/AbilityConfig.cs` | The data; `[TypeSelector]` on the effect string so the plain Inspector gets the same picker | | `Scripts/Effects/` | The candidate types the picker offers | diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/README.md.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/README.md.meta similarity index 100% rename from Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/README.md.meta rename to Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/README.md.meta diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/README.ru.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/README.ru.md similarity index 69% rename from Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/README.ru.md rename to Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/README.ru.md index 51d0db88..3fd9c120 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/README.ru.md +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/README.ru.md @@ -1,6 +1,6 @@ # Пример EditorTools -Окно редактора и кастомный инспектор для небольшого типа ассета `AbilityConfig`, собранные целиком в коде на editor-хелперах пакета: fluent-расширениях `VisualElement`, сеттерах `SerializedProperty`, хелперах отображаемых имён и окне выбора типа как публичном API. Справочники — [VisualElement Extensions](../../Documentation/ru/07-visual-element-extensions.md), [SerializedProperty Extensions](../../Documentation/ru/08-serialized-property-extensions.md), [Editor Helpers](../../Documentation/ru/09-editor-helpers.md) и [TypeSelectorWindow](../../Documentation/ru/02-serializable-types.md#typeselectorwindow). +Окно редактора и кастомный инспектор для небольшого типа ассета `AbilityConfig`, собранные целиком в коде на editor-хелперах пакета: fluent-расширениях `VisualElement`, сеттерах `SerializedProperty`, хелперах отображаемых имён и окне выбора типа как публичном API. Справочники — [VisualElement Extensions](../../../Documentation/ru/07-visual-element-extensions.md), [SerializedProperty Extensions](../../../Documentation/ru/08-serialized-property-extensions.md), [Editor Helpers](../../../Documentation/ru/09-editor-helpers.md) и [TypeSelectorWindow](../../../Documentation/ru/02-serializable-types.md#typeselectorwindow). ```csharp rootVisualElement @@ -9,18 +9,26 @@ rootVisualElement .AddChild(details.SetFlexGrow(1)); ``` +Выберите способность, чтобы изменить её свойства и увидеть описание эффекта. + ## Как открыть 1. Импортируйте пример. Сцены нет. 2. Откройте **Tools → Aspid 🐍 → FastTools → Samples → Ability Catalog**. Левая панель перечисляет четыре ассета `AbilityConfig` из `Data/`; выберите один. +Выберите **Theme → Editor / Dark / Light** в заголовке окна, чтобы сменить палитру для записи. **Editor** использует тему редактора Unity. + +![Halve cooldown, +5 MP обновляет поля и описание эффекта; Undo возвращает прежние значения.](Images/demo.gif) + +Halve cooldown, +5 MP обновляет поля и описание эффекта; Undo возвращает прежние значения. + ## Попробуйте 1. **ListView в пять вызовов.** `SetItemsSource`, `SetMakeItem`, `SetBindItem`, `SetFixedItemHeight`, `AddSelectionChanged`: весь список — одна цепочка. Наберите что-нибудь в поле поиска; `AddValueChanged` фильтрует источник, `RefreshItems` перерисовывает. 2. **Биндинг.** Поля правой панели — обычные `PropertyField` в одном контейнере с `.BindTo(serializedObject)`. Измените имя: список обновится, ассет станет dirty, Undo работает, а стандартный инспектор показывает то же значение. 3. **Типизированные сеттеры свойств.** Нажмите **Halve cooldown, +5 MP**. Обработчик выстраивает цепочку `SetFloat` и `SetIntAndApply` на `SerializedProperty`, а не трогает ассет напрямую, поэтому изменение — один шаг Undo и попадает в файл. -4. **Пикер типов из кода.** Нажмите **Change…** рядом с `Effect`. `TypeSelectorWindow.Show` открывает то же окно с поиском, что и `[TypeSelector]`, привязанное к кнопке и отфильтрованное до реализаций `IAbilityEffect`; результат записывается в `string`-свойство. Выберите `HealEffect`. -5. **Отображаемые имена и доступ к скрипту.** Заголовок панели — `config.GetScriptName()`, который учитывает `[AddComponentMenu]`, если он есть. Дважды кликните по нему: `AddOpenScriptCommand` откроет `AbilityConfig.cs` в вашей IDE. +4. **Пикер типов из кода.** Нажмите **Change…** рядом с `Effect`. `TypeSelectorWindow.Show` открывает то же окно с поиском, что и `[TypeSelector]`, привязанное к кнопке и отфильтрованное до реализаций `IAbilityEffect`; результат записывается в `string`-свойство. Выберите `HealEffect`. Измените `Mana Cost` в окне или инспекторе, затем выполните Undo/Redo: описание эффекта обновляется вместе с данными. +5. **Отображаемые имена и доступ к скрипту.** Заголовок панели — `config.GetDisplayName()`, который учитывает `[AddComponentMenu]`, если он есть. Дважды кликните по нему: `AddOpenScriptCommand` откроет `AbilityConfig.cs` в вашей IDE. 6. **Инспектор.** Выберите `Data/Sprint.asset` в окне Project. `AbilityConfigEditor` рисует карточку со статусным бейджем и предупреждающим `HelpBox`, который виден только пока `Mana Cost` равен `0`; и тем и другим управляет `PropertyField.AddValueChanged`. Поставьте стоимость `10` и верните обратно. 7. **Create.** Нажмите **Create**, чтобы добавить ассет рядом с выбранным; он появится в списке уже выбранным. @@ -28,7 +36,7 @@ rootVisualElement | Файл | Что показывает | |---|---| -| `Scripts/Editor/AbilityCatalogWindow.cs` | Расширения `ListView`, `BindTo`, `SetFloat` / `SetIntAndApply`, `TypeSelectorWindow.Show` с `TypeSelectorFilter`, `GetScriptName`, `AddOpenScriptCommand` | +| `Scripts/Editor/AbilityCatalogWindow.cs` | Расширения `ListView`, `BindTo`, `SetFloat` / `SetIntAndApply`, `TypeSelectorWindow.Show` с `TypeSelectorFilter`, `GetDisplayName`, `AddOpenScriptCommand` | | `Scripts/Editor/AbilityConfigEditor.cs` | Реактивный кастомный инспектор на сеттерах стиля и раскладки | | `Scripts/AbilityConfig.cs` | Данные; `[TypeSelector]` на строке эффекта, чтобы обычный инспектор получил тот же пикер | | `Scripts/Effects/` | Типы-кандидаты, которые предлагает пикер | diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/README.ru.md.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/README.ru.md.meta similarity index 100% rename from Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/README.ru.md.meta rename to Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Documentation/README.ru.md.meta diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts.meta new file mode 100644 index 00000000..664b9ed8 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b28cec2d35974a05a540740b00464e3d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/IBMPlexMono-Regular.ttf b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/IBMPlexMono-Regular.ttf new file mode 100644 index 00000000..4254c37f Binary files /dev/null and b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/IBMPlexMono-Regular.ttf differ diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/IBMPlexMono-Regular.ttf.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/IBMPlexMono-Regular.ttf.meta new file mode 100644 index 00000000..2ef53f5e --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/IBMPlexMono-Regular.ttf.meta @@ -0,0 +1,21 @@ +fileFormatVersion: 2 +guid: bd8b537f2264411383a83ddd21cd7f31 +TrueTypeFontImporter: + externalObjects: {} + serializedVersion: 4 + fontSize: 16 + forceTextureCase: -2 + characterSpacing: 0 + characterPadding: 1 + includeFontData: 1 + fontNames: + - IBM Plex Mono + fallbackFontReferences: [] + customCharacters: + fontRenderingMode: 0 + ascentCalculationMode: 1 + useLegacyBoundsCalculation: 0 + shouldRoundAdvanceValue: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/IBMPlexSerif-Regular.ttf b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/IBMPlexSerif-Regular.ttf new file mode 100644 index 00000000..588d8db4 Binary files /dev/null and b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/IBMPlexSerif-Regular.ttf differ diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/IBMPlexSerif-Regular.ttf.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/IBMPlexSerif-Regular.ttf.meta new file mode 100644 index 00000000..726fe0f6 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/IBMPlexSerif-Regular.ttf.meta @@ -0,0 +1,21 @@ +fileFormatVersion: 2 +guid: 21d53f124f56410191cb8d10b10999ca +TrueTypeFontImporter: + externalObjects: {} + serializedVersion: 4 + fontSize: 16 + forceTextureCase: -2 + characterSpacing: 0 + characterPadding: 1 + includeFontData: 1 + fontNames: + - IBM Plex Serif + fallbackFontReferences: [] + customCharacters: + fontRenderingMode: 0 + ascentCalculationMode: 1 + useLegacyBoundsCalculation: 0 + shouldRoundAdvanceValue: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/LICENSE-IBM-Plex.txt b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/LICENSE-IBM-Plex.txt new file mode 100644 index 00000000..c35c4c61 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/LICENSE-IBM-Plex.txt @@ -0,0 +1,93 @@ +Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. + +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/LICENSE-IBM-Plex.txt.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/LICENSE-IBM-Plex.txt.meta new file mode 100644 index 00000000..ef34ad5a --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/LICENSE-IBM-Plex.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 578fdf41772e48f78cdfc9d8c3b6f877 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/LICENSE-iA-Writer-Quattro.md b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/LICENSE-iA-Writer-Quattro.md new file mode 100644 index 00000000..5cd41aa4 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/LICENSE-iA-Writer-Quattro.md @@ -0,0 +1,100 @@ +# iA Writer Typeface + +Copyright © 2018 Information Architects Inc. with Reserved Font Name "iA Writer" + +# Based on IBM Plex Typeface + +Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" + +# License + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/LICENSE-iA-Writer-Quattro.md.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/LICENSE-iA-Writer-Quattro.md.meta new file mode 100644 index 00000000..0724490f --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/LICENSE-iA-Writer-Quattro.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: dbbc06200f8a4d158f176affb9291232 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/iAWriterQuattroS-Regular.ttf b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/iAWriterQuattroS-Regular.ttf new file mode 100644 index 00000000..f8eb282a Binary files /dev/null and b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/iAWriterQuattroS-Regular.ttf differ diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/iAWriterQuattroS-Regular.ttf.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/iAWriterQuattroS-Regular.ttf.meta new file mode 100644 index 00000000..226dcc7f --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Fonts/iAWriterQuattroS-Regular.ttf.meta @@ -0,0 +1,21 @@ +fileFormatVersion: 2 +guid: 5bf057efea2a4c6886682d9f4de64dc3 +TrueTypeFontImporter: + externalObjects: {} + serializedVersion: 4 + fontSize: 16 + forceTextureCase: -2 + characterSpacing: 0 + characterPadding: 1 + includeFontData: 1 + fontNames: + - iA Writer Quattro S + fallbackFontReferences: [] + customCharacters: + fontRenderingMode: 0 + ascentCalculationMode: 1 + useLegacyBoundsCalculation: 0 + shouldRoundAdvanceValue: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Scripts/AbilityConfig.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Scripts/AbilityConfig.cs index 1d554844..888fb0e2 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Scripts/AbilityConfig.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Scripts/AbilityConfig.cs @@ -5,28 +5,53 @@ // ReSharper disable once CheckNamespace namespace Aspid.FastTools.Samples.EditorTools { - // Plain data. Everything visual lives in Editor/: a custom inspector and a catalog window. + /// + /// that stores ability settings and a selectable effect type. + /// [CreateAssetMenu(menuName = "Aspid/FastTools/Samples/Ability Config", fileName = "Ability")] public sealed class AbilityConfig : ScriptableObject { + [Tooltip("Ability name shown in the catalog.")] [SerializeField] private string _abilityName = "New Ability"; - [SerializeField] [TextArea] private string _description; - [SerializeField] [Min(0)] private int _manaCost = 10; - [SerializeField] [Min(0f)] private float _cooldown = 1f; + + [Tooltip("Description shown in the ability details.")] + [SerializeField, TextArea] private string _description; + + [Tooltip("Mana consumed by the ability.")] + [SerializeField, Min(0)] private int _manaCost = 10; + + [Tooltip("Seconds between ability uses.")] + [SerializeField, Min(0f)] private float _cooldown = 1f; // Written by the catalog window through TypeSelectorWindow; the attribute gives the plain inspector // the same picker. [TypeSelector(typeof(IAbilityEffect), Allow = TypeAllow.None)] + [Tooltip("Effect type used to describe the ability.")] [SerializeField] private string _effectType; + /// + /// Gets the name shown in the ability catalog. + /// public string AbilityName => _abilityName; + /// + /// Gets the ability description. + /// public string Description => _description; + /// + /// Gets the mana cost per use. + /// public int ManaCost => _manaCost; + /// + /// Gets the cooldown in seconds. + /// public float Cooldown => _cooldown; + /// + /// Gets the selected effect type, or when its name is empty or cannot be resolved. + /// public Type EffectType => string.IsNullOrEmpty(_effectType) ? null : Type.GetType(_effectType); } } diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Scripts/Editor/AbilityCatalog.uss b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Scripts/Editor/AbilityCatalog.uss new file mode 100644 index 00000000..6c40d0c3 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Scripts/Editor/AbilityCatalog.uss @@ -0,0 +1,83 @@ +@import url("SampleFonts.uss"); + +.ability-catalog { + --catalog-bg: #10151b; + --catalog-fg: #e8eef1; + --catalog-header: #182129; + --catalog-line: #2c3941; + --catalog-accent: #a6dc81; + --catalog-title: #f1f5f7; + --catalog-muted: #a9b8c2; + --catalog-sidebar: #141c23; + --catalog-button: #25333e; + --catalog-border: #425462; + --catalog-button-text: #edf3f6; + --catalog-hover: #354b59; + --catalog-accent-ink: #17251c; + --catalog-accent-hover: #b9ed95; + --catalog-row-line: #26323b; + --catalog-row-title: #e5edef; + --catalog-secondary: #a4b7c2; + --catalog-selected: #2b4037; + --catalog-field-label: #b9c9d2; + --catalog-input: #0c1217; + --catalog-input-border: #3b4d59; +} +.ability-catalog--light { + --catalog-bg: #eef0f3; + --catalog-fg: #2a2c31; + --catalog-header: #e4e7ec; + --catalog-line: #cdd2da; + --catalog-accent: #8dbf9b; + --catalog-title: #2a2c31; + --catalog-muted: #5f636b; + --catalog-sidebar: #e7eaf0; + --catalog-button: #e0e4eb; + --catalog-border: #b4bdc8; + --catalog-button-text: #303740; + --catalog-hover: #d4e3d9; + --catalog-accent-ink: #233d2c; + --catalog-accent-hover: #a6d8a8; + --catalog-row-line: #d2d7df; + --catalog-row-title: #303740; + --catalog-secondary: #656d78; + --catalog-selected: #d4e6d9; + --catalog-field-label: #505b69; + --catalog-input: #ffffff; + --catalog-input-border: #bdc5d0; +} +.ability-catalog { background-color: var(--catalog-bg); color: var(--catalog-fg); } +.ability-header { padding: 24px 24px 22px; border-bottom-width: 1px; border-bottom-color: var(--catalog-line); background-color: var(--catalog-header); } +#catalogTitle { font-size: 30px; -unity-font-style: bold; color: var(--catalog-title); } +#catalogSubtitle { color: var(--catalog-muted); font-size: 12px; margin-top: 6px; } +.ability-sidebar { background-color: var(--catalog-sidebar); } +.ability-toolbar { margin: 12px 6px; } +.ability-catalog .unity-button { padding: 6px 12px; background-color: var(--catalog-button); border-color: var(--catalog-border); border-width: 1px; border-radius: 5px; color: var(--catalog-button-text); } +.ability-catalog .unity-button:hover { background-color: var(--catalog-hover); border-color: var(--catalog-accent); } +.ability-catalog .ability-primary { background-color: var(--catalog-accent); color: var(--catalog-accent-ink); border-color: var(--catalog-accent); -unity-font-style: bold; } +.ability-catalog .ability-primary:hover { background-color: var(--catalog-accent-hover); color: var(--catalog-accent-ink); } +.ability-row { flex-grow: 1; justify-content: center; padding: 10px 12px; border-bottom-width: 1px; border-bottom-color: var(--catalog-row-line); } +#abilityName { font-size: 14px; -unity-font-style: bold; color: var(--catalog-row-title); } +#abilityStats { font-size: 11px; color: var(--catalog-secondary); margin-top: 5px; } +.ability-catalog .unity-collection-view__item--selected { background-color: var(--catalog-selected); border-left-color: var(--catalog-accent); border-left-width: 3px; } +.ability-details .unity-base-field { margin-bottom: 10px; } +.ability-details .unity-base-field__label { color: var(--catalog-field-label); } +.ability-catalog .unity-base-text-field__input { background-color: var(--catalog-input); color: var(--catalog-button-text); border-color: var(--catalog-input-border); border-width: 1px; border-radius: 4px; padding: 5px; } +.ability-hint { white-space: normal; font-size: 11px; color: var(--catalog-secondary); margin-top: 22px; } + +.ability-details { padding: 28px 20px 20px; } +.ability-detail-title { margin-left: 3px; } +.ability-details .unity-base-field__label { width: 120px; min-width: 120px; padding-right: 8px; } +.ability-details .ability-description { flex-direction: row; align-items: flex-start; } +.ability-description .unity-base-field__label { padding-top: 6px; } +.ability-description .unity-base-text-field__input { min-height: 58px; white-space: normal; } +.ability-actions { margin-top: 12px; margin-left: 4px; align-items: flex-start; } +.ability-catalog .ability-action { width: 196px; height: 30px; margin: 0 0 6px; } + +.ability-effect-row { margin-left: 3px; } + +.ability-catalog .unity-enum-field__input { background-color: var(--catalog-input); border-color: var(--catalog-input-border); color: var(--catalog-button-text); } +.ability-catalog .unity-enum-field__text { color: var(--catalog-button-text); } +.ability-header .unity-base-field__label { color: var(--catalog-muted); } + +.ability-catalog .unity-enum-field__arrow { -unity-background-image-tint-color: var(--catalog-muted); } diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Scripts/Editor/AbilityCatalog.uss.meta b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Scripts/Editor/AbilityCatalog.uss.meta new file mode 100644 index 00000000..c5ab5ff2 --- /dev/null +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Scripts/Editor/AbilityCatalog.uss.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: d6d941128257466a8150a2a242b6badb +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12388, guid: 0000000000000000e000000000000000, type: 0} diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Scripts/Editor/AbilityCatalogWindow.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Scripts/Editor/AbilityCatalogWindow.cs index be83c3b8..9f94c992 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Scripts/Editor/AbilityCatalogWindow.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Samples~/EditorTools/Scripts/Editor/AbilityCatalogWindow.cs @@ -6,8 +6,8 @@ using UnityEngine.UIElements; using Aspid.FastTools.Editors; using Aspid.FastTools.UIElements; -using Aspid.FastTools.Types.Editors; using System.Collections.Generic; +using Aspid.FastTools.Types.Editors; using Aspid.FastTools.UIElements.Editors; // ReSharper disable once CheckNamespace @@ -18,6 +18,8 @@ namespace Aspid.FastTools.Samples.EditorTools.Editors // UI Toolkit binding, so Undo and dirty tracking work as in the Inspector. internal sealed class AbilityCatalogWindow : EditorWindow { + private enum PreviewTheme { Editor, Dark, Light } + private const string ThemeKey = "Aspid.FastTools.AbilityCatalog.Theme"; private readonly List _all = new(); private readonly List _filtered = new(); @@ -27,10 +29,20 @@ internal sealed class AbilityCatalogWindow : EditorWindow [MenuItem("Tools/Aspid 🐍/FastTools/Samples/Ability Catalog")] private static void Open() => - GetWindow("Ability Catalog").minSize = new Vector2(560, 320); + GetWindow("Ability Catalog").minSize = new Vector2(680, 440); private void CreateGUI() { + rootVisualElement.Clear(); + rootVisualElement.AddToClassList("ability-catalog"); + var theme = (PreviewTheme)SessionState.GetInt(ThemeKey, 0); + ApplyTheme(theme); + var scriptPath = AssetDatabase.GetAssetPath(MonoScript.FromScriptableObject(this)); + var stylesheet = AssetDatabase.LoadAssetAtPath( + System.IO.Path.GetDirectoryName(scriptPath) + "/AbilityCatalog.uss"); + if (stylesheet != null) + rootVisualElement.styleSheets.Add(stylesheet); + Reload(); var search = new TextField() @@ -45,35 +57,78 @@ private void CreateGUI() .AddClicked(CreateAsset); var toolbar = new VisualElement() - .SetFlexDirection(FlexDirection.Row).SetAlignItems(Align.Center) - .SetPaddingX(6).SetPaddingY(4) + .SetFlexDirection(FlexDirection.Row) + .SetAlignItems(Align.Center) + .SetPaddingX(6) + .SetPaddingY(4) .AddChild(search) .AddChild(create); _list = new ListView() .SetItemsSource(_filtered) - .SetFixedItemHeight(22) + .SetFixedItemHeight(64) .SetSelectionType(SelectionType.Single) - .SetShowAlternatingRowBackgrounds(AlternatingRowBackground.ContentOnly) - .SetMakeItem(() => new Label().SetPaddingX(6).SetUnityTextAlign(TextAnchor.MiddleLeft)) - .SetBindItem((element, index) => ((Label)element).SetText(_filtered[index].AbilityName)) + .SetShowAlternatingRowBackgrounds(AlternatingRowBackground.None) + .SetMakeItem(() => + { + var row = new VisualElement(); + row.AddToClassList("ability-row"); + row.Add(new Label { name = "abilityName" }); + row.Add(new Label { name = "abilityStats" }); + return row; + }) + .SetBindItem((element, index) => + { + var ability = _filtered[index]; + element.Q