760 lines
19 KiB
Dart
760 lines
19 KiB
Dart
/**
|
||
* 内存优化方案 - 实现代码
|
||
*
|
||
* 优化策略:
|
||
* 1. 对象池优化(减少 GC 压力)
|
||
* 2. 懒加载策略(视口外卸载)
|
||
* 3. 图片/资源缓存管理
|
||
*
|
||
* @version 1.0.0
|
||
* @date 2026-03-07
|
||
* @author 性能优化专家
|
||
*/
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'dart:collection';
|
||
import '../domain/models/core_models.dart';
|
||
|
||
// ============================================================================
|
||
// 1. 对象池优化
|
||
// ============================================================================
|
||
|
||
/// 通用对象池
|
||
class ObjectPool<T extends Poolable> {
|
||
final Queue<T> _pool = Queue();
|
||
final int maxSize;
|
||
final T Function() _factory;
|
||
final void Function(T)? _reset;
|
||
|
||
int _createdCount = 0;
|
||
int _recycledCount = 0;
|
||
|
||
ObjectPool({
|
||
required this.maxSize,
|
||
required T Function() factory,
|
||
this.reset,
|
||
}) : _factory = factory;
|
||
|
||
/// 从池中获取对象
|
||
T acquire() {
|
||
if (_pool.isNotEmpty) {
|
||
_recycledCount++;
|
||
final obj = _pool.removeFirst();
|
||
_reset?.call(obj);
|
||
return obj;
|
||
}
|
||
|
||
_createdCount++;
|
||
return _factory();
|
||
}
|
||
|
||
/// 归还对象到池中
|
||
void release(T obj) {
|
||
if (_pool.length < maxSize) {
|
||
obj.onRecycle();
|
||
_pool.add(obj);
|
||
}
|
||
}
|
||
|
||
/// 清空池
|
||
void clear() {
|
||
_pool.clear();
|
||
}
|
||
|
||
/// 池统计信息
|
||
PoolStats get stats => PoolStats(
|
||
createdCount: _createdCount,
|
||
recycledCount: _recycledCount,
|
||
poolSize: _pool.length,
|
||
maxSize: maxSize,
|
||
);
|
||
|
||
/// 预填充池
|
||
void prefill(int count) {
|
||
for (int i = 0; i < count; i++) {
|
||
_pool.add(_factory());
|
||
_createdCount++;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 池化对象接口
|
||
abstract class Poolable {
|
||
void onRecycle();
|
||
}
|
||
|
||
/// 池统计信息
|
||
class PoolStats {
|
||
final int createdCount;
|
||
final int recycledCount;
|
||
final int poolSize;
|
||
final int maxSize;
|
||
|
||
PoolStats({
|
||
required this.createdCount,
|
||
required this.recycledCount,
|
||
required this.poolSize,
|
||
required this.maxSize,
|
||
});
|
||
|
||
double get recycleRate =>
|
||
createdCount + recycledCount > 0
|
||
? recycledCount / (createdCount + recycledCount)
|
||
: 0.0;
|
||
|
||
@override
|
||
String toString() {
|
||
return 'PoolStats(created: $createdCount, recycled: $recycledCount, '
|
||
'pool: $poolSize/$maxSize, rate: ${(recycleRate * 100).toStringAsFixed(1)}%)';
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// 1.1 元件对象池
|
||
// ============================================================================
|
||
|
||
/// 可池化的元件数据
|
||
class PooledComponentData implements Poolable {
|
||
ID id = '';
|
||
String name = '';
|
||
Position2D position = const Position2D(x: 0, y: 0);
|
||
int rotation = 0;
|
||
List<PinReference> pins = [];
|
||
bool isSelected = false;
|
||
bool isHovered = false;
|
||
|
||
@override
|
||
void onRecycle() {
|
||
id = '';
|
||
name = '';
|
||
position = const Position2D(x: 0, y: 0);
|
||
rotation = 0;
|
||
pins = [];
|
||
isSelected = false;
|
||
isHovered = false;
|
||
}
|
||
|
||
void copyFrom(Component component) {
|
||
id = component.id;
|
||
name = component.name;
|
||
position = component.position;
|
||
rotation = component.rotation;
|
||
pins = component.pins;
|
||
}
|
||
}
|
||
|
||
/// 元件对象池管理器
|
||
class ComponentObjectPool {
|
||
final ObjectPool<PooledComponentData> _pool;
|
||
|
||
ComponentObjectPool({int poolSize = 200})
|
||
: _pool = ObjectPool(
|
||
maxSize: poolSize,
|
||
factory: () => PooledComponentData(),
|
||
);
|
||
|
||
PooledComponentData acquire() => _pool.acquire();
|
||
void release(PooledComponentData data) => _pool.release(data);
|
||
|
||
/// 批量获取
|
||
List<PooledComponentData> acquireBatch(int count) {
|
||
return List.generate(count, (_) => acquire());
|
||
}
|
||
|
||
/// 批量归还
|
||
void releaseBatch(Iterable<PooledComponentData> items) {
|
||
for (final item in items) {
|
||
release(item);
|
||
}
|
||
}
|
||
|
||
PoolStats get stats => _pool.stats;
|
||
|
||
void prefill(int count) => _pool.prefill(count);
|
||
void clear() => _pool.clear();
|
||
}
|
||
|
||
// ============================================================================
|
||
// 1.2 Paint 对象池(减少 CustomPainter 分配)
|
||
// ============================================================================
|
||
|
||
/// Paint 对象池
|
||
class PaintObjectPool {
|
||
final Map<String, Queue<Paint>> _paintPools = {};
|
||
final int _maxSizePerType;
|
||
|
||
PaintObjectPool({int maxSizePerType = 50}) : _maxSizePerType = maxSizePerType;
|
||
|
||
Paint acquire(String type) {
|
||
_paintPools.putIfAbsent(type, () => Queue());
|
||
final pool = _paintPools[type]!;
|
||
|
||
if (pool.isNotEmpty) {
|
||
return pool.removeFirst();
|
||
}
|
||
|
||
return Paint();
|
||
}
|
||
|
||
void release(String type, Paint paint) {
|
||
_paintPools.putIfAbsent(type, () => Queue());
|
||
final pool = _paintPools[type]!;
|
||
|
||
if (pool.length < _maxSizePerType) {
|
||
// 重置 Paint 状态
|
||
paint
|
||
..color = const Color(0xFF000000)
|
||
..strokeWidth = 1.0
|
||
..style = PaintingStyle.fill;
|
||
pool.add(paint);
|
||
}
|
||
}
|
||
|
||
void clear() {
|
||
_paintPools.clear();
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// 2. 懒加载策略
|
||
// ============================================================================
|
||
|
||
/// 视口配置
|
||
class ViewportConfig {
|
||
final double x;
|
||
final double y;
|
||
final double width;
|
||
final double height;
|
||
final double zoomLevel;
|
||
final double margin;
|
||
|
||
const ViewportConfig({
|
||
required this.x,
|
||
required this.y,
|
||
required this.width,
|
||
required this.height,
|
||
required this.zoomLevel,
|
||
this.margin = 100.0,
|
||
});
|
||
|
||
/// 获取可见区域(带边距)
|
||
Rect get bounds => Rect.fromLTWH(
|
||
x - margin / zoomLevel,
|
||
y - margin / zoomLevel,
|
||
width + (2 * margin / zoomLevel),
|
||
height + (2 * margin / zoomLevel),
|
||
);
|
||
|
||
/// 检查点是否在视口内
|
||
bool containsPoint(double px, double py) {
|
||
return px >= bounds.left &&
|
||
px <= bounds.right &&
|
||
py >= bounds.top &&
|
||
py <= bounds.bottom;
|
||
}
|
||
|
||
/// 检查矩形是否与视口相交
|
||
bool intersectsRect(Rect rect) {
|
||
return bounds.overlaps(rect);
|
||
}
|
||
}
|
||
|
||
/// 懒加载缓存
|
||
class LazyLoadCache {
|
||
final Map<ID, Component> _loadedComponents = {};
|
||
final Set<ID> _pendingLoads = {};
|
||
final int _maxLoadedComponents;
|
||
final Queue<ID> _accessOrder = Queue();
|
||
|
||
LazyLoadCache({int maxLoadedComponents = 500})
|
||
: _maxLoadedComponents = maxLoadedComponents;
|
||
|
||
/// 获取已加载的元件
|
||
Component? get(ID id) => _loadedComponents[id];
|
||
|
||
/// 检查是否已加载
|
||
bool isLoaded(ID id) => _loadedComponents.containsKey(id);
|
||
|
||
/// 标记为已加载
|
||
void set(ID id, Component component) {
|
||
// 如果超出容量,移除最久未使用的
|
||
while (_loadedComponents.length >= _maxLoadedComponents) {
|
||
final oldestId = _accessOrder.removeFirst();
|
||
_loadedComponents.remove(oldestId);
|
||
}
|
||
|
||
_loadedComponents[id] = component;
|
||
_accessOrder.remove(id);
|
||
_accessOrder.addLast(id);
|
||
}
|
||
|
||
/// 批量加载
|
||
void loadAll(Map<ID, Component> components) {
|
||
for (final entry in components.entries) {
|
||
set(entry.key, entry.value);
|
||
}
|
||
}
|
||
|
||
/// 卸载视口外的元件
|
||
void unloadOutsideViewport(ViewportConfig viewport) {
|
||
final toUnload = <ID>[];
|
||
|
||
_loadedComponents.forEach((id, component) {
|
||
final posX = component.position.x.toDouble();
|
||
final posY = component.position.y.toDouble();
|
||
|
||
if (!viewport.containsPoint(posX, posY)) {
|
||
toUnload.add(id);
|
||
}
|
||
});
|
||
|
||
for (final id in toUnload) {
|
||
_loadedComponents.remove(id);
|
||
_accessOrder.remove(id);
|
||
}
|
||
|
||
debugPrint('懒加载:卸载了 ${toUnload.length} 个视口外元件');
|
||
}
|
||
|
||
/// 预加载视口附近的元件
|
||
void preloadNearby(
|
||
Map<ID, Component> allComponents,
|
||
ViewportConfig viewport,
|
||
) {
|
||
final preloadMargin = viewport.margin * 2;
|
||
final preloadBounds = Rect.fromLTWH(
|
||
viewport.bounds.left - preloadMargin,
|
||
viewport.bounds.top - preloadMargin,
|
||
viewport.bounds.width + (2 * preloadMargin),
|
||
viewport.bounds.height + (2 * preloadMargin),
|
||
);
|
||
|
||
for (final entry in allComponents.entries) {
|
||
if (_loadedComponents.containsKey(entry.key)) continue;
|
||
|
||
final posX = entry.value.position.x.toDouble();
|
||
final posY = entry.value.position.y.toDouble();
|
||
|
||
if (preloadBounds.contains(Offset(posX, posY))) {
|
||
set(entry.key, entry.value);
|
||
}
|
||
}
|
||
}
|
||
|
||
int get loadedCount => _loadedComponents.length;
|
||
void clear() {
|
||
_loadedComponents.clear();
|
||
_accessOrder.clear();
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// 3. 图片/资源缓存管理
|
||
// ============================================================================
|
||
|
||
/// 图片缓存配置
|
||
class ImageCacheConfig {
|
||
final int maxMemoryBytes;
|
||
final int maxItemCount;
|
||
final Duration expiryDuration;
|
||
|
||
const ImageCacheConfig({
|
||
this.maxMemoryBytes = 50 * 1024 * 1024, // 50MB
|
||
this.maxItemCount = 200,
|
||
this.expiryDuration = const Duration(minutes: 5),
|
||
});
|
||
}
|
||
|
||
/// 缓存的图片
|
||
class CachedImage {
|
||
final ImageProvider provider;
|
||
final int sizeBytes;
|
||
final DateTime loadedAt;
|
||
int accessCount = 0;
|
||
DateTime lastAccessedAt;
|
||
|
||
CachedImage({
|
||
required this.provider,
|
||
required this.sizeBytes,
|
||
required this.loadedAt,
|
||
}) : lastAccessedAt = loadedAt;
|
||
|
||
void access() {
|
||
accessCount++;
|
||
lastAccessedAt = DateTime.now();
|
||
}
|
||
|
||
bool get isExpired {
|
||
return DateTime.now().difference(loadedAt) > const Duration(minutes: 5);
|
||
}
|
||
}
|
||
|
||
/// 图片缓存管理器
|
||
class ImageCacheManager {
|
||
final Map<String, CachedImage> _cache = {};
|
||
final ImageCacheConfig _config;
|
||
int _currentMemoryUsage = 0;
|
||
|
||
ImageCacheManager({ImageCacheConfig? config})
|
||
: _config = config ?? const ImageCacheConfig();
|
||
|
||
/// 获取图片
|
||
ImageProvider? get(String key) {
|
||
final cached = _cache[key];
|
||
if (cached == null) return null;
|
||
|
||
cached.access();
|
||
return cached.provider;
|
||
}
|
||
|
||
/// 缓存图片
|
||
void put(String key, ImageProvider provider, {int sizeBytes = 0}) {
|
||
if (_cache.containsKey(key)) {
|
||
_cache[key]!.access();
|
||
return;
|
||
}
|
||
|
||
// 检查内存限制
|
||
while (_currentMemoryUsage + sizeBytes > _config.maxMemoryBytes ||
|
||
_cache.length >= _config.maxItemCount) {
|
||
_evictLeastUsed();
|
||
}
|
||
|
||
_cache[key] = CachedImage(
|
||
provider: provider,
|
||
sizeBytes: sizeBytes,
|
||
loadedAt: DateTime.now(),
|
||
);
|
||
_currentMemoryUsage += sizeBytes;
|
||
}
|
||
|
||
/// 移除最少使用的图片
|
||
void _evictLeastUsed() {
|
||
if (_cache.isEmpty) return;
|
||
|
||
String? leastUsedKey;
|
||
int leastUsedScore = double.maxFinite.toInt();
|
||
|
||
_cache.forEach((key, cached) {
|
||
// 评分:综合考虑访问次数和最后访问时间
|
||
final score = cached.accessCount +
|
||
(DateTime.now().difference(cached.lastAccessedAt).inSeconds ~/ 10);
|
||
|
||
if (score < leastUsedScore) {
|
||
leastUsedScore = score;
|
||
leastUsedKey = key;
|
||
}
|
||
});
|
||
|
||
if (leastUsedKey != null) {
|
||
final removed = _cache.remove(leastUsedKey)!;
|
||
_currentMemoryUsage -= removed.sizeBytes;
|
||
}
|
||
}
|
||
|
||
/// 清除过期缓存
|
||
void clearExpired() {
|
||
final expiredKeys = <String>[];
|
||
|
||
_cache.forEach((key, cached) {
|
||
if (cached.isExpired) {
|
||
expiredKeys.add(key);
|
||
}
|
||
});
|
||
|
||
for (final key in expiredKeys) {
|
||
final removed = _cache.remove(key)!;
|
||
_currentMemoryUsage -= removed.sizeBytes;
|
||
}
|
||
|
||
if (expiredKeys.isNotEmpty) {
|
||
debugPrint('图片缓存:清除了 ${expiredKeys.length} 个过期项');
|
||
}
|
||
}
|
||
|
||
/// 清除所有缓存
|
||
void clear() {
|
||
_cache.clear();
|
||
_currentMemoryUsage = 0;
|
||
}
|
||
|
||
/// 缓存统计
|
||
CacheStats get stats => CacheStats(
|
||
itemCount: _cache.length,
|
||
memoryBytes: _currentMemoryUsage,
|
||
maxItems: _config.maxItemCount,
|
||
maxMemory: _config.maxMemoryBytes,
|
||
);
|
||
}
|
||
|
||
/// 缓存统计
|
||
class CacheStats {
|
||
final int itemCount;
|
||
final int memoryBytes;
|
||
final int maxItems;
|
||
final int maxMemory;
|
||
|
||
CacheStats({
|
||
required this.itemCount,
|
||
required this.memoryBytes,
|
||
required this.maxItems,
|
||
required this.maxMemory,
|
||
});
|
||
|
||
double get memoryUsagePercent =>
|
||
maxMemory > 0 ? (memoryBytes / maxMemory * 100) : 0.0;
|
||
|
||
double get itemUsagePercent =>
|
||
maxItems > 0 ? (itemCount / maxItems * 100) : 0.0;
|
||
|
||
@override
|
||
String toString() {
|
||
return 'CacheStats(items: $itemCount/$maxItems (${itemUsagePercent.toStringAsFixed(1)}%), '
|
||
'memory: ${(memoryBytes / 1024 / 1024).toStringAsFixed(2)}MB/${(maxMemory / 1024 / 1024)}MB '
|
||
'(${memoryUsagePercent.toStringAsFixed(1)}%))';
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// 4. 优化的画布渲染器(集成所有优化)
|
||
// ============================================================================
|
||
|
||
/// 优化的画布渲染配置
|
||
class OptimizedCanvasConfig {
|
||
final bool enableObjectPooling;
|
||
final bool enableLazyLoading;
|
||
final bool enableImageCaching;
|
||
final int componentPoolSize;
|
||
final int maxLoadedComponents;
|
||
final ImageCacheConfig imageCacheConfig;
|
||
|
||
const OptimizedCanvasConfig({
|
||
this.enableObjectPooling = true,
|
||
this.enableLazyLoading = true,
|
||
this.enableImageCaching = true,
|
||
this.componentPoolSize = 200,
|
||
this.maxLoadedComponents = 500,
|
||
this.imageCacheConfig = const ImageCacheConfig(),
|
||
});
|
||
}
|
||
|
||
/// 优化的画布渲染器
|
||
class OptimizedCanvasRenderer {
|
||
final OptimizedCanvasConfig _config;
|
||
|
||
// 对象池
|
||
late ComponentObjectPool _componentPool;
|
||
late PaintObjectPool _paintPool;
|
||
|
||
// 懒加载缓存
|
||
late LazyLoadCache _lazyLoadCache;
|
||
|
||
// 图片缓存
|
||
late ImageCacheManager _imageCache;
|
||
|
||
// 当前视口
|
||
ViewportConfig? _currentViewport;
|
||
|
||
OptimizedCanvasRenderer({OptimizedCanvasConfig? config})
|
||
: _config = config ?? const OptimizedCanvasConfig() {
|
||
_initialize();
|
||
}
|
||
|
||
void _initialize() {
|
||
if (_config.enableObjectPooling) {
|
||
_componentPool = ComponentObjectPool(
|
||
poolSize: _config.componentPoolSize,
|
||
);
|
||
_paintPool = PaintObjectPool();
|
||
}
|
||
|
||
if (_config.enableLazyLoading) {
|
||
_lazyLoadCache = LazyLoadCache(
|
||
maxLoadedComponents: _config.maxLoadedComponents,
|
||
);
|
||
}
|
||
|
||
if (_config.enableImageCaching) {
|
||
_imageCache = ImageCacheManager(
|
||
config: _config.imageCacheConfig,
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 更新视口
|
||
void updateViewport(ViewportConfig viewport) {
|
||
_currentViewport = viewport;
|
||
|
||
if (_config.enableLazyLoading) {
|
||
// 卸载视口外元件
|
||
_lazyLoadCache.unloadOutsideViewport(viewport);
|
||
|
||
// 预加载附近元件
|
||
// _lazyLoadCache.preloadNearby(allComponents, viewport);
|
||
}
|
||
}
|
||
|
||
/// 获取元件(可能从懒加载缓存)
|
||
Component? getComponent(ID id, Component? fullComponent) {
|
||
if (!_config.enableLazyLoading) return fullComponent;
|
||
|
||
final cached = _lazyLoadCache.get(id);
|
||
if (cached != null) return cached;
|
||
|
||
// 如果不在缓存中,加载并返回完整数据
|
||
if (fullComponent != null) {
|
||
_lazyLoadCache.set(id, fullComponent);
|
||
}
|
||
return fullComponent;
|
||
}
|
||
|
||
/// 获取 Paint(从对象池)
|
||
Paint getPaint(String type) {
|
||
if (_config.enableObjectPooling) {
|
||
return _paintPool.acquire(type);
|
||
}
|
||
return Paint();
|
||
}
|
||
|
||
/// 归还 Paint 到对象池
|
||
void releasePaint(String type, Paint paint) {
|
||
if (_config.enableObjectPooling) {
|
||
_paintPool.release(type, paint);
|
||
}
|
||
}
|
||
|
||
/// 缓存图片
|
||
void cacheImage(String key, ImageProvider provider, {int sizeBytes = 0}) {
|
||
if (_config.enableImageCaching) {
|
||
_imageCache.put(key, provider, sizeBytes: sizeBytes);
|
||
}
|
||
}
|
||
|
||
/// 获取缓存的图片
|
||
ImageProvider? getCachedImage(String key) {
|
||
if (!_config.enableImageCaching) return null;
|
||
return _imageCache.get(key);
|
||
}
|
||
|
||
/// 获取性能统计
|
||
PerformanceStats get performanceStats => PerformanceStats(
|
||
componentPoolStats: _config.enableObjectPooling
|
||
? _componentPool.stats
|
||
: null,
|
||
lazyLoadCacheSize: _config.enableLazyLoading
|
||
? _lazyLoadCache.loadedCount
|
||
: 0,
|
||
imageCacheStats: _config.enableImageCaching
|
||
? _imageCache.stats
|
||
: null,
|
||
);
|
||
|
||
/// 清理资源
|
||
void dispose() {
|
||
if (_config.enableObjectPooling) {
|
||
_componentPool.clear();
|
||
_paintPool.clear();
|
||
}
|
||
if (_config.enableLazyLoading) {
|
||
_lazyLoadCache.clear();
|
||
}
|
||
if (_config.enableImageCaching) {
|
||
_imageCache.clear();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 性能统计
|
||
class PerformanceStats {
|
||
final PoolStats? componentPoolStats;
|
||
final int lazyLoadCacheSize;
|
||
final CacheStats? imageCacheStats;
|
||
|
||
PerformanceStats({
|
||
this.componentPoolStats,
|
||
required this.lazyLoadCacheSize,
|
||
this.imageCacheStats,
|
||
});
|
||
|
||
@override
|
||
String toString() {
|
||
return 'PerformanceStats(\n'
|
||
' Component Pool: ${componentPoolStats ?? "disabled"},\n'
|
||
' Lazy Load Cache: $lazyLoadCacheSize components,\n'
|
||
' Image Cache: ${imageCacheStats ?? "disabled"}\n'
|
||
')';
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// 5. 使用示例
|
||
// ============================================================================
|
||
|
||
/*
|
||
/// 在 EditableCanvas 中使用优化
|
||
class OptimizedEditableCanvas extends StatefulWidget {
|
||
final Design design;
|
||
final Function(Design) onDesignChanged;
|
||
final SelectionManager selectionManager;
|
||
final OptimizedCanvasConfig config;
|
||
|
||
const OptimizedEditableCanvas({
|
||
required this.design,
|
||
required this.onDesignChanged,
|
||
required this.selectionManager,
|
||
this.config = const OptimizedCanvasConfig(),
|
||
});
|
||
|
||
@override
|
||
State<OptimizedEditableCanvas> createState() => _OptimizedEditableCanvasState();
|
||
}
|
||
|
||
class _OptimizedEditableCanvasState extends State<OptimizedEditableCanvas> {
|
||
late OptimizedCanvasRenderer _renderer;
|
||
double _zoomLevel = 1.0;
|
||
Offset _offset = Offset.zero;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_renderer = OptimizedCanvasRenderer(config: widget.config);
|
||
|
||
// 预填充对象池
|
||
if (widget.config.enableObjectPooling) {
|
||
// 预填充 100 个元件对象
|
||
}
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_renderer.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return LayoutBuilder(
|
||
builder: (context, constraints) {
|
||
// 更新视口
|
||
_renderer.updateViewport(ViewportConfig(
|
||
x: -_offset.dx / _zoomLevel,
|
||
y: -_offset.dy / _zoomLevel,
|
||
width: constraints.maxWidth / _zoomLevel,
|
||
height: constraints.maxHeight / _zoomLevel,
|
||
zoomLevel: _zoomLevel,
|
||
));
|
||
|
||
return CustomPaint(
|
||
painter: OptimizedSchematicPainter(
|
||
design: widget.design,
|
||
renderer: _renderer,
|
||
zoomLevel: _zoomLevel,
|
||
offset: _offset,
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|
||
*/
|