mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 00:54:26 +08:00
0.3.5
This commit is contained in:
28
LanMountainDesktop/ComponentSystem/BuiltInComponentIds.cs
Normal file
28
LanMountainDesktop/ComponentSystem/BuiltInComponentIds.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace LanMountainDesktop.ComponentSystem;
|
||||
|
||||
public static class BuiltInComponentIds
|
||||
{
|
||||
public const string Clock = "Clock";
|
||||
public const string DesktopClock = "DesktopClock";
|
||||
public const string DesktopWeatherClock = "DesktopWeatherClock";
|
||||
public const string DesktopTimer = "DesktopTimer";
|
||||
public const string DesktopWeather = "DesktopWeather";
|
||||
public const string DesktopHourlyWeather = "DesktopHourlyWeather";
|
||||
public const string DesktopMultiDayWeather = "DesktopMultiDayWeather";
|
||||
public const string DesktopExtendedWeather = "DesktopExtendedWeather";
|
||||
public const string DesktopClassSchedule = "DesktopClassSchedule";
|
||||
public const string DesktopMusicControl = "DesktopMusicControl";
|
||||
public const string DesktopAudioRecorder = "DesktopAudioRecorder";
|
||||
public const string DesktopStudyEnvironment = "DesktopStudyEnvironment";
|
||||
public const string DesktopStudyNoiseCurve = "DesktopStudyNoiseCurve";
|
||||
public const string Blank2x4 = "Blank2x4";
|
||||
public const string Date = "Date";
|
||||
public const string MonthCalendar = "MonthCalendar";
|
||||
public const string LunarCalendar = "LunarCalendar";
|
||||
public const string HolidayCalendar = "HolidayCalendar";
|
||||
public const string DesktopDailyPoetry = "DesktopDailyPoetry";
|
||||
public const string DesktopDailyArtwork = "DesktopDailyArtwork";
|
||||
public const string DesktopWhiteboard = "DesktopWhiteboard";
|
||||
public const string DesktopBlackboardLandscape = "DesktopBlackboardLandscape";
|
||||
public const string DesktopBrowser = "DesktopBrowser";
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
|
||||
namespace LanMountainDesktop.ComponentSystem;
|
||||
|
||||
public static class ComponentPlacementRules
|
||||
{
|
||||
public static (int WidthCells, int HeightCells) EnsureMinimumSize(
|
||||
DesktopComponentDefinition definition,
|
||||
int requestedWidthCells,
|
||||
int requestedHeightCells)
|
||||
{
|
||||
var width = Math.Max(definition.MinWidthCells, requestedWidthCells);
|
||||
var height = Math.Max(definition.MinHeightCells, requestedHeightCells);
|
||||
return (Math.Max(1, width), Math.Max(1, height));
|
||||
}
|
||||
|
||||
public static bool CanPlaceInStatusBar(DesktopComponentDefinition definition, int requestedHeightCells)
|
||||
{
|
||||
return definition.AllowStatusBarPlacement && requestedHeightCells == 1;
|
||||
}
|
||||
|
||||
public static (int Column, int Row) ClampToGrid(int requestedColumn, int requestedRow, int maxColumns, int maxRows)
|
||||
{
|
||||
var clampedColumn = Math.Clamp(requestedColumn, 0, Math.Max(0, maxColumns - 1));
|
||||
var clampedRow = Math.Clamp(requestedRow, 0, Math.Max(0, maxRows - 1));
|
||||
return (clampedColumn, clampedRow);
|
||||
}
|
||||
}
|
||||
268
LanMountainDesktop/ComponentSystem/ComponentRegistry.cs
Normal file
268
LanMountainDesktop/ComponentSystem/ComponentRegistry.cs
Normal file
@@ -0,0 +1,268 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using LanMountainDesktop.ComponentSystem.Extensions;
|
||||
|
||||
namespace LanMountainDesktop.ComponentSystem;
|
||||
|
||||
public sealed class ComponentRegistry
|
||||
{
|
||||
private readonly Dictionary<string, DesktopComponentDefinition> _definitions;
|
||||
|
||||
public ComponentRegistry(IEnumerable<DesktopComponentDefinition> definitions)
|
||||
{
|
||||
_definitions = definitions
|
||||
.Where(d => !string.IsNullOrWhiteSpace(d.Id))
|
||||
.GroupBy(d => d.Id, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(g => g.Key, g => g.Last(), StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static ComponentRegistry CreateDefault()
|
||||
{
|
||||
var builtIn = new[]
|
||||
{
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.Clock,
|
||||
"Clock",
|
||||
"Clock",
|
||||
"Status",
|
||||
MinWidthCells: 3,
|
||||
MinHeightCells: 1,
|
||||
AllowStatusBarPlacement: true,
|
||||
AllowDesktopPlacement: false),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopClock,
|
||||
"Clock",
|
||||
"Clock",
|
||||
"Clock",
|
||||
MinWidthCells: 2,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopWeatherClock,
|
||||
"Weather Clock",
|
||||
"Clock",
|
||||
"Clock",
|
||||
MinWidthCells: 2,
|
||||
MinHeightCells: 1,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopTimer,
|
||||
"Timer",
|
||||
"Timer",
|
||||
"Clock",
|
||||
MinWidthCells: 2,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopWeather,
|
||||
"Weather",
|
||||
"WeatherSunny",
|
||||
"Weather",
|
||||
MinWidthCells: 2,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopHourlyWeather,
|
||||
"Hourly Weather",
|
||||
"WeatherSunny",
|
||||
"Weather",
|
||||
MinWidthCells: 4,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopMultiDayWeather,
|
||||
"Multi-day Weather",
|
||||
"WeatherSunny",
|
||||
"Weather",
|
||||
MinWidthCells: 4,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopExtendedWeather,
|
||||
"Extended Weather",
|
||||
"WeatherSunny",
|
||||
"Weather",
|
||||
MinWidthCells: 4,
|
||||
MinHeightCells: 4,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopClassSchedule,
|
||||
"Class Schedule",
|
||||
"CalendarDate",
|
||||
"Date",
|
||||
MinWidthCells: 2,
|
||||
MinHeightCells: 4,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true,
|
||||
ResizeMode: DesktopComponentResizeMode.Free),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopMusicControl,
|
||||
"Music Control",
|
||||
"Play",
|
||||
"Media",
|
||||
MinWidthCells: 4,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopAudioRecorder,
|
||||
"Recorder",
|
||||
"MicOn",
|
||||
"Media",
|
||||
MinWidthCells: 2,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopStudyEnvironment,
|
||||
"Study Environment",
|
||||
"MicOn",
|
||||
"Study",
|
||||
MinWidthCells: 2,
|
||||
MinHeightCells: 1,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true,
|
||||
ResizeMode: DesktopComponentResizeMode.Free),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopStudyNoiseCurve,
|
||||
"Noise Curve",
|
||||
"DataLine",
|
||||
"Study",
|
||||
MinWidthCells: 4,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopDailyPoetry,
|
||||
"Daily Poetry",
|
||||
"Book",
|
||||
"Info",
|
||||
MinWidthCells: 4,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopDailyArtwork,
|
||||
"Daily Artwork",
|
||||
"Image",
|
||||
"Info",
|
||||
MinWidthCells: 4,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopWhiteboard,
|
||||
"Blackboard Portrait",
|
||||
"Edit",
|
||||
"Board",
|
||||
MinWidthCells: 2,
|
||||
MinHeightCells: 4,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true,
|
||||
ResizeMode: DesktopComponentResizeMode.Free),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopBlackboardLandscape,
|
||||
"Blackboard Landscape",
|
||||
"Edit",
|
||||
"Board",
|
||||
MinWidthCells: 4,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true,
|
||||
ResizeMode: DesktopComponentResizeMode.Free),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopBrowser,
|
||||
"Browser",
|
||||
"Globe",
|
||||
"Board",
|
||||
MinWidthCells: 4,
|
||||
MinHeightCells: 4,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true,
|
||||
ResizeMode: DesktopComponentResizeMode.Free),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.Date,
|
||||
"Calendar",
|
||||
"Calendar",
|
||||
"Date",
|
||||
MinWidthCells: 4,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.MonthCalendar,
|
||||
"Month Calendar",
|
||||
"CalendarMonth",
|
||||
"Date",
|
||||
MinWidthCells: 2,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.LunarCalendar,
|
||||
"Lunar Calendar",
|
||||
"Calendar",
|
||||
"Date",
|
||||
MinWidthCells: 2,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.HolidayCalendar,
|
||||
"Holiday Countdown",
|
||||
"Calendar",
|
||||
"Date",
|
||||
MinWidthCells: 2,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true)
|
||||
};
|
||||
|
||||
return new ComponentRegistry(builtIn);
|
||||
}
|
||||
|
||||
public ComponentRegistry RegisterExtensions(IEnumerable<IComponentExtensionProvider> providers)
|
||||
{
|
||||
var merged = _definitions.Values.ToList();
|
||||
foreach (var provider in providers)
|
||||
{
|
||||
var externalDefinitions = provider.GetComponents();
|
||||
if (externalDefinitions is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
merged.AddRange(externalDefinitions);
|
||||
}
|
||||
|
||||
return new ComponentRegistry(merged);
|
||||
}
|
||||
|
||||
public bool TryGetDefinition(string componentId, out DesktopComponentDefinition definition)
|
||||
{
|
||||
return _definitions.TryGetValue(componentId, out definition!);
|
||||
}
|
||||
|
||||
public bool IsKnownComponent(string componentId)
|
||||
{
|
||||
return _definitions.ContainsKey(componentId);
|
||||
}
|
||||
|
||||
public bool AllowsStatusBarPlacement(string componentId)
|
||||
{
|
||||
return _definitions.TryGetValue(componentId, out var definition) && definition.AllowStatusBarPlacement;
|
||||
}
|
||||
|
||||
public IReadOnlyList<DesktopComponentDefinition> GetAll()
|
||||
{
|
||||
return _definitions.Values.OrderBy(d => d.Category).ThenBy(d => d.DisplayName).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace LanMountainDesktop.ComponentSystem;
|
||||
|
||||
public sealed record DesktopComponentDefinition(
|
||||
string Id,
|
||||
string DisplayName,
|
||||
string IconKey,
|
||||
string Category,
|
||||
int MinWidthCells,
|
||||
int MinHeightCells,
|
||||
bool AllowStatusBarPlacement,
|
||||
bool AllowDesktopPlacement,
|
||||
DesktopComponentResizeMode ResizeMode = DesktopComponentResizeMode.Proportional);
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace LanMountainDesktop.ComponentSystem;
|
||||
|
||||
public enum DesktopComponentResizeMode
|
||||
{
|
||||
Proportional = 0,
|
||||
Free = 1
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LanMountainDesktop.ComponentSystem.Extensions;
|
||||
|
||||
public interface IComponentExtensionProvider
|
||||
{
|
||||
IReadOnlyList<DesktopComponentDefinition> GetComponents();
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace LanMountainDesktop.ComponentSystem.Extensions;
|
||||
|
||||
public sealed class JsonComponentExtensionProvider : IComponentExtensionProvider
|
||||
{
|
||||
private readonly IReadOnlyList<DesktopComponentDefinition> _definitions;
|
||||
|
||||
private JsonComponentExtensionProvider(IReadOnlyList<DesktopComponentDefinition> definitions)
|
||||
{
|
||||
_definitions = definitions;
|
||||
}
|
||||
|
||||
public IReadOnlyList<DesktopComponentDefinition> GetComponents()
|
||||
{
|
||||
return _definitions;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<IComponentExtensionProvider> LoadProvidersFromDirectory(string directoryPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(directoryPath) || !Directory.Exists(directoryPath))
|
||||
{
|
||||
return Array.Empty<IComponentExtensionProvider>();
|
||||
}
|
||||
|
||||
var providers = new List<IComponentExtensionProvider>();
|
||||
foreach (var filePath in Directory.GetFiles(directoryPath, "*.json", SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
var provider = TryLoadFromFile(filePath);
|
||||
if (provider is not null)
|
||||
{
|
||||
providers.Add(provider);
|
||||
}
|
||||
}
|
||||
|
||||
return providers;
|
||||
}
|
||||
|
||||
private static JsonComponentExtensionProvider? TryLoadFromFile(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(filePath);
|
||||
var entries = JsonSerializer.Deserialize<List<ComponentExtensionEntry>>(json);
|
||||
if (entries is null || entries.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var definitions = new List<DesktopComponentDefinition>();
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(entry.Id) ||
|
||||
string.IsNullOrWhiteSpace(entry.DisplayName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
definitions.Add(new DesktopComponentDefinition(
|
||||
entry.Id.Trim(),
|
||||
entry.DisplayName.Trim(),
|
||||
string.IsNullOrWhiteSpace(entry.IconKey) ? "PuzzlePiece" : entry.IconKey,
|
||||
string.IsNullOrWhiteSpace(entry.Category) ? "Extensions" : entry.Category,
|
||||
MinWidthCells: Math.Max(1, entry.MinWidthCells),
|
||||
MinHeightCells: Math.Max(1, entry.MinHeightCells),
|
||||
AllowStatusBarPlacement: entry.AllowStatusBarPlacement,
|
||||
AllowDesktopPlacement: entry.AllowDesktopPlacement));
|
||||
}
|
||||
|
||||
return definitions.Count == 0 ? null : new JsonComponentExtensionProvider(definitions);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ComponentExtensionEntry
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
public string IconKey { get; set; } = string.Empty;
|
||||
|
||||
public string Category { get; set; } = "Extensions";
|
||||
|
||||
public int MinWidthCells { get; set; } = 1;
|
||||
|
||||
public int MinHeightCells { get; set; } = 1;
|
||||
|
||||
public bool AllowStatusBarPlacement { get; set; }
|
||||
|
||||
public bool AllowDesktopPlacement { get; set; } = true;
|
||||
}
|
||||
}
|
||||
77
LanMountainDesktop/ComponentSystem/README.md
Normal file
77
LanMountainDesktop/ComponentSystem/README.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# 组件系统模块(Component System Module)
|
||||
|
||||
本目录提供组件系统的模块化基础,用于支持内置组件管理与第三方扩展接入。
|
||||
This directory provides the modular foundation for built-in component management and third-party extension integration.
|
||||
|
||||
## 核心文件职责(Core Files)
|
||||
- `BuiltInComponentIds.cs`:内置组件 ID 常量(例如 `Clock`)。
|
||||
Built-in component ID constants (for example `Clock`).
|
||||
- `DesktopComponentDefinition.cs`:组件元数据定义(名称、类别、最小尺寸、可放置区域等)。
|
||||
Component metadata model (name, category, minimum size, placement permissions).
|
||||
- `ComponentPlacementRules.cs`:组件放置规则(最小尺寸、状态栏高度限制、网格边界约束)。
|
||||
Placement rules (minimum size, status-bar height rule, grid clamping).
|
||||
- `ComponentRegistry.cs`:组件注册中心,负责内置组件与扩展组件合并。
|
||||
Registry that merges built-in and extension components.
|
||||
- `Extensions/IComponentExtensionProvider.cs`:扩展提供者接口契约。
|
||||
Extension provider interface contract.
|
||||
- `Extensions/JsonComponentExtensionProvider.cs`:基于 JSON 的扩展加载器。
|
||||
JSON-based extension loader.
|
||||
|
||||
## 第三方扩展契约(Extension Contract)
|
||||
- 第三方可通过实现 `IComponentExtensionProvider` 提供组件定义。
|
||||
Third parties can provide component definitions via `IComponentExtensionProvider`.
|
||||
- 当前内置了 JSON 提供者,运行时扫描目录:
|
||||
Built-in JSON provider scans at runtime:
|
||||
- `Extensions/Components/*.json`(相对应用输出目录)
|
||||
`Extensions/Components/*.json` (relative to app output directory)
|
||||
|
||||
## 加载流程(Load Flow)
|
||||
1. `ComponentRegistry.CreateDefault()` 先注册内置组件。
|
||||
Register built-in components first via `ComponentRegistry.CreateDefault()`.
|
||||
2. 调用 `.RegisterExtensions(...)` 合并扩展组件。
|
||||
Merge extension components via `.RegisterExtensions(...)`.
|
||||
3. 主窗口通过注册中心校验组件合法性与放置权限。
|
||||
Main window validates component identity and placement permission through the registry.
|
||||
|
||||
## JSON 清单格式(Manifest Schema)
|
||||
JSON 文件为数组,每一项代表一个组件定义。
|
||||
The JSON file is an array, where each item represents one component definition.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "Weather",
|
||||
"displayName": "Weather",
|
||||
"iconKey": "WeatherSunny",
|
||||
"category": "Status",
|
||||
"minWidthCells": 1,
|
||||
"minHeightCells": 1,
|
||||
"allowStatusBarPlacement": true,
|
||||
"allowDesktopPlacement": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
字段说明(Field notes):
|
||||
- `id`:组件唯一 ID(建议英文、稳定不变)。
|
||||
Unique component ID (prefer stable English key).
|
||||
- `displayName`:显示名。
|
||||
Display name.
|
||||
- `iconKey`:图标键(由上层 UI 解释)。
|
||||
Icon key resolved by UI layer.
|
||||
- `category`:组件分类。
|
||||
Component category.
|
||||
- `minWidthCells` / `minHeightCells`:最小占格,必须满足 `>= 1`。
|
||||
Minimum cell size, must satisfy `>= 1`.
|
||||
- `allowStatusBarPlacement`:是否允许放到顶部状态栏。
|
||||
Whether placing in top status bar is allowed.
|
||||
- `allowDesktopPlacement`:是否允许放到桌面区域。
|
||||
Whether placing in desktop area is allowed.
|
||||
|
||||
## 放置规则摘要(Placement Rules Summary)
|
||||
- 最小尺寸约束:`minWidthCells >= 1` 且 `minHeightCells >= 1`。
|
||||
Minimum size constraint: `minWidthCells >= 1` and `minHeightCells >= 1`.
|
||||
- 状态栏约束:状态栏组件高度必须为 `1` 格。
|
||||
Status bar constraint: component height must be exactly `1` cell.
|
||||
- 越界约束:所有组件坐标会被网格边界钳制(clamp)。
|
||||
Out-of-bounds constraint: component coordinates are clamped to grid bounds.
|
||||
Reference in New Issue
Block a user