Files
ichni_Official/Packages/NB_FX/NBShaders2/Editor/NBShaderGUI_RULE.md

336 lines
15 KiB
Markdown
Raw Normal View History

2026-07-17 17:46:16 -04:00
# NBShaderGUI Rule
## Purpose
This rule defines the structure constraints for `NBShaders2/Editor/NBShaderGUI` and shared `ShaderGUIItems`.
## Core Principles
- Keep the GUI structure minimal and explicit.
- Do not add wrapper abstractions without a clear responsibility.
- `FoldOut` behavior and `parent-child composition` are two different concerns and must not be mixed.
- Common drawing items live in `XuanXuanRenderUtility/Editor/ShaderGUIItems`.
- Shared item names must not use the `NBShader` prefix.
## Shared Item Rules
All common material-property drawing items must live in `XuanXuanRenderUtility/Editor/ShaderGUIItems`, not in `NBShaders2/Editor/ShaderGUIItems`.
Use shared items for common material property drawing:
- `ToggleItem`
- `ColorItem`
- `TextureItem`
- `Vector2LineItem`
- `VectorComponentItem`
- `HelpBoxItem`
Vector-related shared item classes should stay in one `VectorItem.cs` file.
Do not create NBShader-prefixed wrappers for common drawing behavior. These names are not allowed for shared/general controls:
- `NBShaderTogglePropertyItem`
- `NBShaderColorPropertyItem`
- `NBShaderTexturePropertyItem`
- `NBShaderVector2LinePropertyItem`
- `NBShaderVectorComponentPropertyItem`
- `NBShaderHelpBoxItem`
Do not create wrappers that only set `PropertyName`, `GuiContent`, ranges, or visibility around an existing shared item. Prefer using the shared item directly, or create a concrete business item when there is real shader-specific behavior.
Pure configuration items should be instantiated directly inside the owning block constructor.
Use direct shared item construction for cases that only assign:
- `PropertyName`
- `GuiContent` or `GUIContent` provider
- `RangePropertyName`
- `Min` / `Max`
- simple visibility predicates
Example pattern:
```csharp
_alphaAllItem = new ShaderGUISliderItem(rootItem, this)
{
PropertyName = "_AlphaAll",
GuiContent = NBShaderInspectorLocalization.MakeInspectorContent("base.alphaAll", "Overall Alpha"),
RangePropertyName = "AlphaAllRangeVec"
};
_alphaAllItem.InitTriggerByChild();
```
Do not create a named class for this pattern, such as `AlphaAllItem`, unless it adds real behavior.
A concrete item class is justified only when it owns shader-specific behavior, such as:
- overriding `OnGUI`, `DrawBlock`, `OnEndChange`, `CheckIsPropertyModified`, or `ExecuteReset`
- synchronizing keywords, render queue, blend state, pass state, runtime flags, or context
- implementing a reusable layout/control behavior that belongs in shared `ShaderGUIItems`
Current examples:
- `ZTestItem` is allowed because it forces UIEffect depth behavior.
- `AddToPreMultiplySlider` is allowed because its reset default depends on `BlendMode`.
- Popup items are allowed when they update `Context`, keywords, render state, or child visibility.
- `BaseColorIntensityItem`, `AlphaAllItem`, and `CutOffSlider` are not allowed as named wrappers because they only configure shared float/slider items.
Shared items must not reference:
- `NBShaderRootItem`
- `NBShaderInspectorLocalization`
- NBShader-specific keyword, pass, or flag logic
NBShader-specific labels should be passed into shared items through `GUIContent` providers from the call site.
## Localization Rules
NBShader Inspector user-facing text must go through `NBShaderInspectorLocalization`.
- Use CSV keys in `NBShaders2/Editor/Localization/NBShaderInspectorLocalization.csv`.
- CSV format is `key,zh-CN,en-US,zh-CN-tip,en-US-tip`.
- Tooltip text belongs to the same row as its label key. If a row has no tooltip, leave the tip columns empty.
- Legacy `.tip` rows are only compatibility fallback for existing non-label message reads. New label tooltips must not create a separate `.tip` key.
- Language selection is project-wide and stored by `NBFXProjectSettings` in `ProjectSettings/NB_FXSettings.asset`.
- Language selection is edited through Project Settings > Project > NB_FX, not through Unity menu items.
Fallback order:
- current language
- `zh-CN`
- code fallback
Key conventions:
- normal labels use `inspector.<block>.<feature>.<name>.label`
- label tooltips use the same key as the label and read the `<language>-tip` column
- standalone tooltip/help text that is not attached to a `GUIContent` should use `.message` or another explicit text key
- pure messages use `inspector.<block>.<feature>.<name>.message`
- buttons use `inspector.<block>.<feature>.<name>.button`
- popup options use `inspector.<block>.<feature>.<name>.option.<index>`
Code conventions:
- Normal controls should call `NBShaderInspectorLocalization.MakeInspectorContent(key, fallback, tip)` or pass a provider that calls it. The helper reads the label from `inspector.<key>.label` and the tooltip from the same CSV row first.
- HelpBox/message strings should call `NBShaderInspectorLocalization.GetInspectorText(key, fallback)`.
- Popup options should call `NBShaderInspectorLocalization.GetInspectorOptions(key, fallbackArray)` or update `PopUpNames` from an options provider.
- Shared `XuanXuanRenderUtility/Editor/ShaderGUIItems` must not reference `NBShaderInspectorLocalization`.
- Shared items that draw visible labels should expose `GUIContent` providers; NBShader call sites provide localized content.
- Technical identifiers stay untranslated unless they are deliberately shown as UI copy: shader property names, keyword names, pass names, enum protocol values, render queue numbers, and stencil config keys remain stable.
## NBShaders2 Item Rules
`NBShaders2/Editor/ShaderGUIItems` should only contain NBShader business structure:
- top-level blocks
- secondary business blocks
- shader-specific popup items with side effects
- composite layout items such as the main texture layout
- feature-specific items that synchronize NBShader keywords, pass state, or runtime flags
NBShader-specific item names are acceptable only when the item has NBShader business meaning. They must not be used for generic drawing controls.
## Inheritance Rules
### 1. `ShaderGUIItem`
`ShaderGUIItem` is the base class for normal GUI items.
Use it for:
- leaf controls
- composite layout items
- parent-child grouping items without foldout
- texture layout parent items
Important:
- parent-child composition already belongs to `ShaderGUIItem`
- do not introduce extra “control base class” layers just to express parent-child grouping
### 2. `BlockItem`
`BlockItem` exists only to provide `FoldOut` behavior.
Responsibilities:
- owns `FoldOutPropertyName`
- owns foldout open/close lifecycle
- draws foldout title row
- draws children inside foldout content area
It must not be used for:
- non-foldout layout items
- generic feature/control wrappers
### 3. `BigBlockItem`
`BigBlockItem` inherits from `BlockItem`.
Responsibilities:
- defines the visual style of top-level blocks
- bold title
- spacing
- separator line
`BigBlockItem` is a style specialization of `BlockItem`, not a different composition system.
## Top-Level Block Rules
These top-level sections should inherit `BigBlockItem`:
- `ModeBigBlockItem`
- `BaseOptionBigBlockItem`
- `MainTexBigBlockItem`
- `LightBigBlockItem`
- `FeatureBigBlockItem`
- `TABigBlockItem`
These are first-level inspector sections and should use persisted foldout state.
## Secondary Block Rules
Only use `BlockItem` for a secondary section when:
- it has its own foldout state
- it is semantically a collapsible section
- it contains a group of child items
If a section is only doing layout and grouping, it must stay on `ShaderGUIItem`.
## Layout Rules
All normal inspector row layout must be owned by `ShaderGUIItem`.
Do not calculate label/control/reset positions independently in leaf items. Use the base rect pipeline:
- `GetRect()`
- `ApplyGlobalRectCompensation()`
- `SplitLineRect()`
- `SplitControlAndResetRect()`
Standard row responsibilities:
- `BaseRect` is the compensated full row rect.
- `LabelRect` uses `GetLabelWidth(BaseRect)`.
- `GetLabelWidth()` has a fixed minimum (`MinLabelWidth`) and then grows by `LabelWidthRatio` once the inspector row is wide enough.
- Do not read or hardcode a fixed label width in business items. If a custom layout needs the normal label/control split, call `SplitLineRect()`.
- `ControlRect` starts after `LabelRect` and reserves reset space.
- `ResetRect` is fixed to the right side and uses `ResetButtonSize`.
- `ControlResetGap` is the only standard gap between control and reset.
Do not add local magic numbers for reset width, reset gap, or standard control expansion in business items. If a layout constant is needed by more than one item, add it to `ShaderGUIItem`.
Global row compensation:
- `GlobalRectXOffset` shifts the whole row left/right.
- `GlobalRectWidthExpansion` expands the whole row width.
- These constants compensate meaningless inspector-side margin and must be applied at the rect entry point, before splitting label/control/reset.
Any item that calls `EditorGUILayout.GetControlRect()` directly must immediately pass the result through `ApplyGlobalRectCompensation()`, unless there is a specific documented reason not to.
Editor indent rules:
- `EditorGUI.indentLevel` is only a hierarchy level counter.
- Do not introduce a separate per-block indent-count constant.
- Unity's internal per-level indent width is represented by `UnityEditorGUIIndentWidth`.
- The desired NBShader GUI visual indent width is represented by `EditorGUIIndentWidth`.
- `ApplyLabelIndentWidth()` converts Unity's built-in `EditorGUI.LabelField` indent width into the desired visual indent width for the NBShader GUI rect system.
- Direct `GUI.Label`, `GUI.Toggle`, texture preview rects, and other non-`EditorGUI.LabelField` labels do not get Unity's built-in label indent. Use `ApplyDirectLabelIndentWidth()` for their label start.
- Foldout arrows must be positioned from the final visual label text x, not from `BaseRect.x`. Use `ShaderGUIFoldOutHelper` / `GetEditorLabelTextX()` and do not call `EditorGUI.Foldout()` directly in business items.
- Do not hardcode a local texture or foldout indent width. Three-line texture groups must use `ApplyDirectLabelIndentWidth()` for the texture preview left edge and `SplitLineRect()` for Tilling/Offset control/reset positions.
Control indent compensation:
- `ControlIndentCompensation` is not the same thing as row indent.
- It only adjusts `ControlRect` by moving `x` left and increasing `width`.
- Use it only to compensate controls whose Unity internal drawing is shifted right inside the passed rect.
- Do not apply it to controls that draw exactly inside the passed rect, such as single-line `ColorField`; call `GetRect(false)` or `SplitLineRect(..., false)` for those rows.
- `EditorGUI.ColorField` draws the visible swatch through `EditorStyles.colorField.padding.Remove(position)`. Labeled color rows may compensate this padding inside `ColorItem`; no-label color rows keep their own local inset. Do not add Color-only constants to `ShaderGUIItem`.
- Keep `LabelRect` and `ResetRect` independent from this compensation.
- Composite layouts may disable it with `applyControlIndentCompensation: false` and then apply a local compensation only to the actual control rect when needed.
Full-width or no-label rows:
- Rows such as full-width color bars should not automatically use label/control semantics.
- If a row has no label, call `SplitControlAndResetRect()` directly and disable control compensation when the row must preserve its left edge.
- Reset alignment should still come from `SplitControlAndResetRect()`.
Texture rows:
- All texture object inputs should use the shared three-row texture layout (`TexturePropertyGroupItem` through `TextureItem`) instead of `MaterialEditor.TexturePropertySingleLine()`.
- Texture rows without editable Tilling/Offset should still reserve the three-row texture object area and leave the Tilling/Offset rows empty.
- Texture color rows remain separate no-label color rows after the three-row texture object group.
## Animated Property Rules
Animated property highlight must follow Unity's native material inspector behavior.
Do not manually set animated controls to a fixed color such as `Color.red`.
Use the shared `ShaderGUIItem` animated scope helpers:
- `BeginAnimatedPropertyBackground(Rect totalPosition, MaterialProperty property)`
- `EndAnimatedPropertyBackground(bool scopeActive)`
These helpers must use `MaterialEditor.BeginAnimatedCheck()` and `MaterialEditor.EndAnimatedCheck()`. They must not implement their own color decision with `AnimationMode.IsPropertyAnimated()`.
Reason:
- Unity's original `MaterialEditor.ShaderProperty()` wraps controls with `BeginAnimatedCheck()` / `EndAnimatedCheck()`.
- Unity chooses between animated, recorded, and candidate colors internally.
- Unity restores the previous `GUI.backgroundColor` through a stack, so nested drawing does not leak color state.
Standard item rule:
- Normal leaf items that use `ShaderGUIItem.OnGUI()` should rely on the base animated scope around `DrawController()`.
Custom `OnGUI()` rule:
- If an item overrides `OnGUI()` or draws multiple independent controls manually, it must explicitly wrap each actual editable control with the correct `MaterialProperty`.
- Do not wrap a composite row with one property if the row contains controls backed by different properties.
- Always call `EndAnimatedPropertyBackground()` in the same draw path after the control is drawn.
Multi-property control rule:
- `ShaderGUISliderItem` with `RangePropertyName` must wrap the min/max fields with the range vector property and the slider with the main float property.
- Texture scale/offset controls must wrap their `Vector2Field` controls with the texture property when editing `textureScaleAndOffset`, or with the vector property when editing a vector ST property.
Animation refresh rule:
- `ShaderGUIRootItem` must update incoming `MaterialProperty[]` into `PropertyInfoDic` every `OnGUI`.
- In animation mode, the material inspector should repaint so animated values and highlight state refresh while scrubbing or recording.
- Do not cache animated-state booleans. Caching property path strings is allowed for diagnostics or non-background queries.
## Texture GUI Rules
For texture-related GUI:
- the texture layout parent item should inherit `ShaderGUIItem`
- it is a layout/composition item, not a `BlockItem`
- texture object field, texture color, and texture `Tilling/Offset` are separate GUI items
- the parent texture layout item is responsible only for arranging these child items
- texture layout must not move label text as a side effect of control compensation
- for `Tilling/Offset`, calculate label rects from the uncompensated vector rect first, then apply control compensation only to the `Vector2Field` rect if visual control alignment requires it
- texture, texture label, texture color, `Tilling/Offset`, and reset buttons must still use shared `ShaderGUIItem` layout constants where applicable
- texture ST animated highlight must use Unity's texture `MaterialProperty` animated check, not manual `_ST.x/y/z/w` color logic
Required structure:
- texture parent layout item
- texture object field item
- texture color item
- texture `Tilling/Offset` item
## Anti-Rules
Do not introduce these kinds of abstractions again unless there is a concrete need:
- generic `ControlItem` base classes with no real behavior
- generic `FeatureItem` base classes with no real semantics
- wrapper layers that only rename existing base classes
If an abstraction does not define a stable responsibility boundary, do not keep it.