diff --git a/app/lib/cubits/current_index.dart b/app/lib/cubits/current_index.dart index 57370a263d7a..7f7f244c6827 100644 --- a/app/lib/cubits/current_index.dart +++ b/app/lib/cubits/current_index.dart @@ -90,6 +90,7 @@ sealed class CurrentIndex with _$CurrentIndex { @Default('') String userName, @Default(false) bool penDetected, @Default(false) bool sessionPenOnlyInput, + @Default(false) bool wireframeMode, }) = _CurrentIndex; /// Returns the effective pen-only input state. @@ -211,6 +212,10 @@ class CurrentIndexCubit extends Cubit { emit(state.copyWith(sessionPenOnlyInput: value)); } + void toggleWireframeMode() { + emit(state.copyWith(wireframeMode: !state.wireframeMode)); + } + Future _updateOnVisible( CameraViewport newViewport, DocumentLoaded blocState, diff --git a/app/lib/cubits/current_index.freezed.dart b/app/lib/cubits/current_index.freezed.dart index f0e37087d32b..f5a8f6bbc23c 100644 --- a/app/lib/cubits/current_index.freezed.dart +++ b/app/lib/cubits/current_index.freezed.dart @@ -14,7 +14,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$CurrentIndex implements DiagnosticableTreeMixin { - int? get index; Handler get handler; CameraViewport get cameraViewport; SettingsCubit get settingsCubit; TransformCubit get transformCubit; NetworkingService get networkingService; bool get isSaveDelayed; UtilitiesState get utilities; Handler? get temporaryHandler; int? get temporaryIndex; List get foregrounds; Selection? get selection; bool get pinned; List? get temporaryForegrounds; Map> get toggleableHandlers; List get networkingForegrounds; Map> get toggleableForegrounds; MouseCursor get cursor; MouseCursor? get temporaryCursor; TemporaryState get temporaryState; Offset? get lastPosition; List get pointers; int? get buttons; AssetLocation get location; Embedding? get embedding; SaveState get saved; PreferredSizeWidget? get toolbar; PreferredSizeWidget? get temporaryToolbar; Map get rendererStates; Map? get temporaryRendererStates; ViewOption get viewOption; HideState get hideUi; bool get areaNavigatorCreate; bool get areaNavigatorExact; bool get areaNavigatorAsk; bool get navigatorEnabled; NavigatorPage get navigatorPage; bool get isCreating; String get userName; bool get penDetected; bool get sessionPenOnlyInput; + int? get index; Handler get handler; CameraViewport get cameraViewport; SettingsCubit get settingsCubit; TransformCubit get transformCubit; NetworkingService get networkingService; bool get isSaveDelayed; UtilitiesState get utilities; Handler? get temporaryHandler; int? get temporaryIndex; List get foregrounds; Selection? get selection; bool get pinned; List? get temporaryForegrounds; Map> get toggleableHandlers; List get networkingForegrounds; Map> get toggleableForegrounds; MouseCursor get cursor; MouseCursor? get temporaryCursor; TemporaryState get temporaryState; Offset? get lastPosition; List get pointers; int? get buttons; AssetLocation get location; Embedding? get embedding; SaveState get saved; PreferredSizeWidget? get toolbar; PreferredSizeWidget? get temporaryToolbar; Map get rendererStates; Map? get temporaryRendererStates; ViewOption get viewOption; HideState get hideUi; bool get areaNavigatorCreate; bool get areaNavigatorExact; bool get areaNavigatorAsk; bool get navigatorEnabled; NavigatorPage get navigatorPage; bool get isCreating; String get userName; bool get penDetected; bool get sessionPenOnlyInput; bool get wireframeMode; /// Create a copy of CurrentIndex /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -26,14 +26,14 @@ $CurrentIndexCopyWith get copyWith => _$CurrentIndexCopyWithImpl { factory $CurrentIndexCopyWith(CurrentIndex value, $Res Function(CurrentIndex) _then) = _$CurrentIndexCopyWithImpl; @useResult $Res call({ - int? index, Handler handler, CameraViewport cameraViewport, SettingsCubit settingsCubit, TransformCubit transformCubit, NetworkingService networkingService, bool isSaveDelayed, UtilitiesState utilities, Handler? temporaryHandler, int? temporaryIndex, List foregrounds, Selection? selection, bool pinned, List? temporaryForegrounds, Map> toggleableHandlers, List networkingForegrounds, Map> toggleableForegrounds, MouseCursor cursor, MouseCursor? temporaryCursor, TemporaryState temporaryState, Offset? lastPosition, List pointers, int? buttons, AssetLocation location, Embedding? embedding, SaveState saved, PreferredSizeWidget? toolbar, PreferredSizeWidget? temporaryToolbar, Map rendererStates, Map? temporaryRendererStates, ViewOption viewOption, HideState hideUi, bool areaNavigatorCreate, bool areaNavigatorExact, bool areaNavigatorAsk, bool navigatorEnabled, NavigatorPage navigatorPage, bool isCreating, String userName, bool penDetected, bool sessionPenOnlyInput + int? index, Handler handler, CameraViewport cameraViewport, SettingsCubit settingsCubit, TransformCubit transformCubit, NetworkingService networkingService, bool isSaveDelayed, UtilitiesState utilities, Handler? temporaryHandler, int? temporaryIndex, List foregrounds, Selection? selection, bool pinned, List? temporaryForegrounds, Map> toggleableHandlers, List networkingForegrounds, Map> toggleableForegrounds, MouseCursor cursor, MouseCursor? temporaryCursor, TemporaryState temporaryState, Offset? lastPosition, List pointers, int? buttons, AssetLocation location, Embedding? embedding, SaveState saved, PreferredSizeWidget? toolbar, PreferredSizeWidget? temporaryToolbar, Map rendererStates, Map? temporaryRendererStates, ViewOption viewOption, HideState hideUi, bool areaNavigatorCreate, bool areaNavigatorExact, bool areaNavigatorAsk, bool navigatorEnabled, NavigatorPage navigatorPage, bool isCreating, String userName, bool penDetected, bool sessionPenOnlyInput, bool wireframeMode }); @@ -61,7 +61,7 @@ class _$CurrentIndexCopyWithImpl<$Res> /// Create a copy of CurrentIndex /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? index = freezed,Object? handler = null,Object? cameraViewport = null,Object? settingsCubit = null,Object? transformCubit = null,Object? networkingService = null,Object? isSaveDelayed = null,Object? utilities = null,Object? temporaryHandler = freezed,Object? temporaryIndex = freezed,Object? foregrounds = null,Object? selection = freezed,Object? pinned = null,Object? temporaryForegrounds = freezed,Object? toggleableHandlers = null,Object? networkingForegrounds = null,Object? toggleableForegrounds = null,Object? cursor = null,Object? temporaryCursor = freezed,Object? temporaryState = null,Object? lastPosition = freezed,Object? pointers = null,Object? buttons = freezed,Object? location = null,Object? embedding = freezed,Object? saved = null,Object? toolbar = freezed,Object? temporaryToolbar = freezed,Object? rendererStates = null,Object? temporaryRendererStates = freezed,Object? viewOption = null,Object? hideUi = null,Object? areaNavigatorCreate = null,Object? areaNavigatorExact = null,Object? areaNavigatorAsk = null,Object? navigatorEnabled = null,Object? navigatorPage = null,Object? isCreating = null,Object? userName = null,Object? penDetected = null,Object? sessionPenOnlyInput = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? index = freezed,Object? handler = null,Object? cameraViewport = null,Object? settingsCubit = null,Object? transformCubit = null,Object? networkingService = null,Object? isSaveDelayed = null,Object? utilities = null,Object? temporaryHandler = freezed,Object? temporaryIndex = freezed,Object? foregrounds = null,Object? selection = freezed,Object? pinned = null,Object? temporaryForegrounds = freezed,Object? toggleableHandlers = null,Object? networkingForegrounds = null,Object? toggleableForegrounds = null,Object? cursor = null,Object? temporaryCursor = freezed,Object? temporaryState = null,Object? lastPosition = freezed,Object? pointers = null,Object? buttons = freezed,Object? location = null,Object? embedding = freezed,Object? saved = null,Object? toolbar = freezed,Object? temporaryToolbar = freezed,Object? rendererStates = null,Object? temporaryRendererStates = freezed,Object? viewOption = null,Object? hideUi = null,Object? areaNavigatorCreate = null,Object? areaNavigatorExact = null,Object? areaNavigatorAsk = null,Object? navigatorEnabled = null,Object? navigatorPage = null,Object? isCreating = null,Object? userName = null,Object? penDetected = null,Object? sessionPenOnlyInput = null,Object? wireframeMode = null,}) { return _then(_self.copyWith( index: freezed == index ? _self.index : index // ignore: cast_nullable_to_non_nullable as int?,handler: null == handler ? _self.handler : handler // ignore: cast_nullable_to_non_nullable @@ -104,6 +104,7 @@ as NavigatorPage,isCreating: null == isCreating ? _self.isCreating : isCreating as bool,userName: null == userName ? _self.userName : userName // ignore: cast_nullable_to_non_nullable as String,penDetected: null == penDetected ? _self.penDetected : penDetected // ignore: cast_nullable_to_non_nullable as bool,sessionPenOnlyInput: null == sessionPenOnlyInput ? _self.sessionPenOnlyInput : sessionPenOnlyInput // ignore: cast_nullable_to_non_nullable +as bool,wireframeMode: null == wireframeMode ? _self.wireframeMode : wireframeMode // ignore: cast_nullable_to_non_nullable as bool, )); } @@ -134,7 +135,7 @@ $ViewOptionCopyWith<$Res> get viewOption { class _CurrentIndex extends CurrentIndex with DiagnosticableTreeMixin { - const _CurrentIndex(this.index, this.handler, this.cameraViewport, this.settingsCubit, this.transformCubit, this.networkingService, {this.isSaveDelayed = false, this.utilities = const UtilitiesState(), this.temporaryHandler, this.temporaryIndex, final List foregrounds = const [], this.selection, this.pinned = false, final List? temporaryForegrounds, final Map> toggleableHandlers = const {}, final List networkingForegrounds = const [], final Map> toggleableForegrounds = const {}, this.cursor = MouseCursor.defer, this.temporaryCursor, this.temporaryState = TemporaryState.allowClick, this.lastPosition, final List pointers = const [], this.buttons, this.location = const AssetLocation(path: ''), this.embedding, this.saved = SaveState.saved, this.toolbar, this.temporaryToolbar, final Map rendererStates = const {}, final Map? temporaryRendererStates = const {}, this.viewOption = const ViewOption(), this.hideUi = HideState.visible, this.areaNavigatorCreate = true, this.areaNavigatorExact = true, this.areaNavigatorAsk = false, this.navigatorEnabled = false, this.navigatorPage = NavigatorPage.waypoints, this.isCreating = false, this.userName = '', this.penDetected = false, this.sessionPenOnlyInput = false}): _foregrounds = foregrounds,_temporaryForegrounds = temporaryForegrounds,_toggleableHandlers = toggleableHandlers,_networkingForegrounds = networkingForegrounds,_toggleableForegrounds = toggleableForegrounds,_pointers = pointers,_rendererStates = rendererStates,_temporaryRendererStates = temporaryRendererStates,super._(); + const _CurrentIndex(this.index, this.handler, this.cameraViewport, this.settingsCubit, this.transformCubit, this.networkingService, {this.isSaveDelayed = false, this.utilities = const UtilitiesState(), this.temporaryHandler, this.temporaryIndex, final List foregrounds = const [], this.selection, this.pinned = false, final List? temporaryForegrounds, final Map> toggleableHandlers = const {}, final List networkingForegrounds = const [], final Map> toggleableForegrounds = const {}, this.cursor = MouseCursor.defer, this.temporaryCursor, this.temporaryState = TemporaryState.allowClick, this.lastPosition, final List pointers = const [], this.buttons, this.location = const AssetLocation(path: ''), this.embedding, this.saved = SaveState.saved, this.toolbar, this.temporaryToolbar, final Map rendererStates = const {}, final Map? temporaryRendererStates = const {}, this.viewOption = const ViewOption(), this.hideUi = HideState.visible, this.areaNavigatorCreate = true, this.areaNavigatorExact = true, this.areaNavigatorAsk = false, this.navigatorEnabled = false, this.navigatorPage = NavigatorPage.waypoints, this.isCreating = false, this.userName = '', this.penDetected = false, this.sessionPenOnlyInput = false, this.wireframeMode = false}): _foregrounds = foregrounds,_temporaryForegrounds = temporaryForegrounds,_toggleableHandlers = toggleableHandlers,_networkingForegrounds = networkingForegrounds,_toggleableForegrounds = toggleableForegrounds,_pointers = pointers,_rendererStates = rendererStates,_temporaryRendererStates = temporaryRendererStates,super._(); @override final int? index; @@ -230,6 +231,7 @@ class _CurrentIndex extends CurrentIndex with DiagnosticableTreeMixin { @override@JsonKey() final String userName; @override@JsonKey() final bool penDetected; @override@JsonKey() final bool sessionPenOnlyInput; +@override@JsonKey() final bool wireframeMode; /// Create a copy of CurrentIndex /// with the given fields replaced by the non-null parameter values. @@ -242,14 +244,14 @@ _$CurrentIndexCopyWith<_CurrentIndex> get copyWith => __$CurrentIndexCopyWithImp void debugFillProperties(DiagnosticPropertiesBuilder properties) { properties ..add(DiagnosticsProperty('type', 'CurrentIndex')) - ..add(DiagnosticsProperty('index', index))..add(DiagnosticsProperty('handler', handler))..add(DiagnosticsProperty('cameraViewport', cameraViewport))..add(DiagnosticsProperty('settingsCubit', settingsCubit))..add(DiagnosticsProperty('transformCubit', transformCubit))..add(DiagnosticsProperty('networkingService', networkingService))..add(DiagnosticsProperty('isSaveDelayed', isSaveDelayed))..add(DiagnosticsProperty('utilities', utilities))..add(DiagnosticsProperty('temporaryHandler', temporaryHandler))..add(DiagnosticsProperty('temporaryIndex', temporaryIndex))..add(DiagnosticsProperty('foregrounds', foregrounds))..add(DiagnosticsProperty('selection', selection))..add(DiagnosticsProperty('pinned', pinned))..add(DiagnosticsProperty('temporaryForegrounds', temporaryForegrounds))..add(DiagnosticsProperty('toggleableHandlers', toggleableHandlers))..add(DiagnosticsProperty('networkingForegrounds', networkingForegrounds))..add(DiagnosticsProperty('toggleableForegrounds', toggleableForegrounds))..add(DiagnosticsProperty('cursor', cursor))..add(DiagnosticsProperty('temporaryCursor', temporaryCursor))..add(DiagnosticsProperty('temporaryState', temporaryState))..add(DiagnosticsProperty('lastPosition', lastPosition))..add(DiagnosticsProperty('pointers', pointers))..add(DiagnosticsProperty('buttons', buttons))..add(DiagnosticsProperty('location', location))..add(DiagnosticsProperty('embedding', embedding))..add(DiagnosticsProperty('saved', saved))..add(DiagnosticsProperty('toolbar', toolbar))..add(DiagnosticsProperty('temporaryToolbar', temporaryToolbar))..add(DiagnosticsProperty('rendererStates', rendererStates))..add(DiagnosticsProperty('temporaryRendererStates', temporaryRendererStates))..add(DiagnosticsProperty('viewOption', viewOption))..add(DiagnosticsProperty('hideUi', hideUi))..add(DiagnosticsProperty('areaNavigatorCreate', areaNavigatorCreate))..add(DiagnosticsProperty('areaNavigatorExact', areaNavigatorExact))..add(DiagnosticsProperty('areaNavigatorAsk', areaNavigatorAsk))..add(DiagnosticsProperty('navigatorEnabled', navigatorEnabled))..add(DiagnosticsProperty('navigatorPage', navigatorPage))..add(DiagnosticsProperty('isCreating', isCreating))..add(DiagnosticsProperty('userName', userName))..add(DiagnosticsProperty('penDetected', penDetected))..add(DiagnosticsProperty('sessionPenOnlyInput', sessionPenOnlyInput)); + ..add(DiagnosticsProperty('index', index))..add(DiagnosticsProperty('handler', handler))..add(DiagnosticsProperty('cameraViewport', cameraViewport))..add(DiagnosticsProperty('settingsCubit', settingsCubit))..add(DiagnosticsProperty('transformCubit', transformCubit))..add(DiagnosticsProperty('networkingService', networkingService))..add(DiagnosticsProperty('isSaveDelayed', isSaveDelayed))..add(DiagnosticsProperty('utilities', utilities))..add(DiagnosticsProperty('temporaryHandler', temporaryHandler))..add(DiagnosticsProperty('temporaryIndex', temporaryIndex))..add(DiagnosticsProperty('foregrounds', foregrounds))..add(DiagnosticsProperty('selection', selection))..add(DiagnosticsProperty('pinned', pinned))..add(DiagnosticsProperty('temporaryForegrounds', temporaryForegrounds))..add(DiagnosticsProperty('toggleableHandlers', toggleableHandlers))..add(DiagnosticsProperty('networkingForegrounds', networkingForegrounds))..add(DiagnosticsProperty('toggleableForegrounds', toggleableForegrounds))..add(DiagnosticsProperty('cursor', cursor))..add(DiagnosticsProperty('temporaryCursor', temporaryCursor))..add(DiagnosticsProperty('temporaryState', temporaryState))..add(DiagnosticsProperty('lastPosition', lastPosition))..add(DiagnosticsProperty('pointers', pointers))..add(DiagnosticsProperty('buttons', buttons))..add(DiagnosticsProperty('location', location))..add(DiagnosticsProperty('embedding', embedding))..add(DiagnosticsProperty('saved', saved))..add(DiagnosticsProperty('toolbar', toolbar))..add(DiagnosticsProperty('temporaryToolbar', temporaryToolbar))..add(DiagnosticsProperty('rendererStates', rendererStates))..add(DiagnosticsProperty('temporaryRendererStates', temporaryRendererStates))..add(DiagnosticsProperty('viewOption', viewOption))..add(DiagnosticsProperty('hideUi', hideUi))..add(DiagnosticsProperty('areaNavigatorCreate', areaNavigatorCreate))..add(DiagnosticsProperty('areaNavigatorExact', areaNavigatorExact))..add(DiagnosticsProperty('areaNavigatorAsk', areaNavigatorAsk))..add(DiagnosticsProperty('navigatorEnabled', navigatorEnabled))..add(DiagnosticsProperty('navigatorPage', navigatorPage))..add(DiagnosticsProperty('isCreating', isCreating))..add(DiagnosticsProperty('userName', userName))..add(DiagnosticsProperty('penDetected', penDetected))..add(DiagnosticsProperty('sessionPenOnlyInput', sessionPenOnlyInput))..add(DiagnosticsProperty('wireframeMode', wireframeMode)); } @override String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { - return 'CurrentIndex(index: $index, handler: $handler, cameraViewport: $cameraViewport, settingsCubit: $settingsCubit, transformCubit: $transformCubit, networkingService: $networkingService, isSaveDelayed: $isSaveDelayed, utilities: $utilities, temporaryHandler: $temporaryHandler, temporaryIndex: $temporaryIndex, foregrounds: $foregrounds, selection: $selection, pinned: $pinned, temporaryForegrounds: $temporaryForegrounds, toggleableHandlers: $toggleableHandlers, networkingForegrounds: $networkingForegrounds, toggleableForegrounds: $toggleableForegrounds, cursor: $cursor, temporaryCursor: $temporaryCursor, temporaryState: $temporaryState, lastPosition: $lastPosition, pointers: $pointers, buttons: $buttons, location: $location, embedding: $embedding, saved: $saved, toolbar: $toolbar, temporaryToolbar: $temporaryToolbar, rendererStates: $rendererStates, temporaryRendererStates: $temporaryRendererStates, viewOption: $viewOption, hideUi: $hideUi, areaNavigatorCreate: $areaNavigatorCreate, areaNavigatorExact: $areaNavigatorExact, areaNavigatorAsk: $areaNavigatorAsk, navigatorEnabled: $navigatorEnabled, navigatorPage: $navigatorPage, isCreating: $isCreating, userName: $userName, penDetected: $penDetected, sessionPenOnlyInput: $sessionPenOnlyInput)'; + return 'CurrentIndex(index: $index, handler: $handler, cameraViewport: $cameraViewport, settingsCubit: $settingsCubit, transformCubit: $transformCubit, networkingService: $networkingService, isSaveDelayed: $isSaveDelayed, utilities: $utilities, temporaryHandler: $temporaryHandler, temporaryIndex: $temporaryIndex, foregrounds: $foregrounds, selection: $selection, pinned: $pinned, temporaryForegrounds: $temporaryForegrounds, toggleableHandlers: $toggleableHandlers, networkingForegrounds: $networkingForegrounds, toggleableForegrounds: $toggleableForegrounds, cursor: $cursor, temporaryCursor: $temporaryCursor, temporaryState: $temporaryState, lastPosition: $lastPosition, pointers: $pointers, buttons: $buttons, location: $location, embedding: $embedding, saved: $saved, toolbar: $toolbar, temporaryToolbar: $temporaryToolbar, rendererStates: $rendererStates, temporaryRendererStates: $temporaryRendererStates, viewOption: $viewOption, hideUi: $hideUi, areaNavigatorCreate: $areaNavigatorCreate, areaNavigatorExact: $areaNavigatorExact, areaNavigatorAsk: $areaNavigatorAsk, navigatorEnabled: $navigatorEnabled, navigatorPage: $navigatorPage, isCreating: $isCreating, userName: $userName, penDetected: $penDetected, sessionPenOnlyInput: $sessionPenOnlyInput, wireframeMode: $wireframeMode)'; } @@ -260,7 +262,7 @@ abstract mixin class _$CurrentIndexCopyWith<$Res> implements $CurrentIndexCopyWi factory _$CurrentIndexCopyWith(_CurrentIndex value, $Res Function(_CurrentIndex) _then) = __$CurrentIndexCopyWithImpl; @override @useResult $Res call({ - int? index, Handler handler, CameraViewport cameraViewport, SettingsCubit settingsCubit, TransformCubit transformCubit, NetworkingService networkingService, bool isSaveDelayed, UtilitiesState utilities, Handler? temporaryHandler, int? temporaryIndex, List foregrounds, Selection? selection, bool pinned, List? temporaryForegrounds, Map> toggleableHandlers, List networkingForegrounds, Map> toggleableForegrounds, MouseCursor cursor, MouseCursor? temporaryCursor, TemporaryState temporaryState, Offset? lastPosition, List pointers, int? buttons, AssetLocation location, Embedding? embedding, SaveState saved, PreferredSizeWidget? toolbar, PreferredSizeWidget? temporaryToolbar, Map rendererStates, Map? temporaryRendererStates, ViewOption viewOption, HideState hideUi, bool areaNavigatorCreate, bool areaNavigatorExact, bool areaNavigatorAsk, bool navigatorEnabled, NavigatorPage navigatorPage, bool isCreating, String userName, bool penDetected, bool sessionPenOnlyInput + int? index, Handler handler, CameraViewport cameraViewport, SettingsCubit settingsCubit, TransformCubit transformCubit, NetworkingService networkingService, bool isSaveDelayed, UtilitiesState utilities, Handler? temporaryHandler, int? temporaryIndex, List foregrounds, Selection? selection, bool pinned, List? temporaryForegrounds, Map> toggleableHandlers, List networkingForegrounds, Map> toggleableForegrounds, MouseCursor cursor, MouseCursor? temporaryCursor, TemporaryState temporaryState, Offset? lastPosition, List pointers, int? buttons, AssetLocation location, Embedding? embedding, SaveState saved, PreferredSizeWidget? toolbar, PreferredSizeWidget? temporaryToolbar, Map rendererStates, Map? temporaryRendererStates, ViewOption viewOption, HideState hideUi, bool areaNavigatorCreate, bool areaNavigatorExact, bool areaNavigatorAsk, bool navigatorEnabled, NavigatorPage navigatorPage, bool isCreating, String userName, bool penDetected, bool sessionPenOnlyInput, bool wireframeMode }); @@ -277,7 +279,7 @@ class __$CurrentIndexCopyWithImpl<$Res> /// Create a copy of CurrentIndex /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? index = freezed,Object? handler = null,Object? cameraViewport = null,Object? settingsCubit = null,Object? transformCubit = null,Object? networkingService = null,Object? isSaveDelayed = null,Object? utilities = null,Object? temporaryHandler = freezed,Object? temporaryIndex = freezed,Object? foregrounds = null,Object? selection = freezed,Object? pinned = null,Object? temporaryForegrounds = freezed,Object? toggleableHandlers = null,Object? networkingForegrounds = null,Object? toggleableForegrounds = null,Object? cursor = null,Object? temporaryCursor = freezed,Object? temporaryState = null,Object? lastPosition = freezed,Object? pointers = null,Object? buttons = freezed,Object? location = null,Object? embedding = freezed,Object? saved = null,Object? toolbar = freezed,Object? temporaryToolbar = freezed,Object? rendererStates = null,Object? temporaryRendererStates = freezed,Object? viewOption = null,Object? hideUi = null,Object? areaNavigatorCreate = null,Object? areaNavigatorExact = null,Object? areaNavigatorAsk = null,Object? navigatorEnabled = null,Object? navigatorPage = null,Object? isCreating = null,Object? userName = null,Object? penDetected = null,Object? sessionPenOnlyInput = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? index = freezed,Object? handler = null,Object? cameraViewport = null,Object? settingsCubit = null,Object? transformCubit = null,Object? networkingService = null,Object? isSaveDelayed = null,Object? utilities = null,Object? temporaryHandler = freezed,Object? temporaryIndex = freezed,Object? foregrounds = null,Object? selection = freezed,Object? pinned = null,Object? temporaryForegrounds = freezed,Object? toggleableHandlers = null,Object? networkingForegrounds = null,Object? toggleableForegrounds = null,Object? cursor = null,Object? temporaryCursor = freezed,Object? temporaryState = null,Object? lastPosition = freezed,Object? pointers = null,Object? buttons = freezed,Object? location = null,Object? embedding = freezed,Object? saved = null,Object? toolbar = freezed,Object? temporaryToolbar = freezed,Object? rendererStates = null,Object? temporaryRendererStates = freezed,Object? viewOption = null,Object? hideUi = null,Object? areaNavigatorCreate = null,Object? areaNavigatorExact = null,Object? areaNavigatorAsk = null,Object? navigatorEnabled = null,Object? navigatorPage = null,Object? isCreating = null,Object? userName = null,Object? penDetected = null,Object? sessionPenOnlyInput = null,Object? wireframeMode = null,}) { return _then(_CurrentIndex( freezed == index ? _self.index : index // ignore: cast_nullable_to_non_nullable as int?,null == handler ? _self.handler : handler // ignore: cast_nullable_to_non_nullable @@ -320,6 +322,7 @@ as NavigatorPage,isCreating: null == isCreating ? _self.isCreating : isCreating as bool,userName: null == userName ? _self.userName : userName // ignore: cast_nullable_to_non_nullable as String,penDetected: null == penDetected ? _self.penDetected : penDetected // ignore: cast_nullable_to_non_nullable as bool,sessionPenOnlyInput: null == sessionPenOnlyInput ? _self.sessionPenOnlyInput : sessionPenOnlyInput // ignore: cast_nullable_to_non_nullable +as bool,wireframeMode: null == wireframeMode ? _self.wireframeMode : wireframeMode // ignore: cast_nullable_to_non_nullable as bool, )); } diff --git a/app/lib/handlers/grid.dart b/app/lib/handlers/grid.dart index 459fed9c2a58..a128fbd1b74d 100644 --- a/app/lib/handlers/grid.dart +++ b/app/lib/handlers/grid.dart @@ -64,6 +64,7 @@ class GridRenderer extends Renderer { CameraTransform transform, [ ColorScheme? colorScheme, bool foreground = false, + bool wireframeMode = false, ]) { if (element.xSize > 0) { double x = -element.xSize + element.xOffset % element.xSize; diff --git a/app/lib/handlers/polygon.dart b/app/lib/handlers/polygon.dart index b6ade035baad..7bc83fc7ab01 100644 --- a/app/lib/handlers/polygon.dart +++ b/app/lib/handlers/polygon.dart @@ -17,6 +17,7 @@ class PolygonSelectionRenderer extends Renderer { CameraTransform transform, [ ColorScheme? colorScheme, bool foreground = false, + bool wireframeMode = false, ]) { if (element.points.isEmpty) return; final strokeWidth = element.property.strokeWidth; diff --git a/app/lib/handlers/ruler.dart b/app/lib/handlers/ruler.dart index 7311b54d3f5d..710451d5bb1f 100644 --- a/app/lib/handlers/ruler.dart +++ b/app/lib/handlers/ruler.dart @@ -123,6 +123,7 @@ class RulerRenderer extends Renderer { CameraTransform transform, [ ColorScheme? colorScheme, bool foreground = false, + bool wireframeMode = false, ]) { canvas.save(); canvas.translate(transform.position.dx, transform.position.dy); diff --git a/app/lib/handlers/spacer.dart b/app/lib/handlers/spacer.dart index 7c8bff6cd428..2decd5ce74d7 100644 --- a/app/lib/handlers/spacer.dart +++ b/app/lib/handlers/spacer.dart @@ -143,6 +143,7 @@ class SpacerRenderer extends Renderer { CameraTransform transform, [ ColorScheme? colorScheme, bool foreground = false, + bool wireframeMode = false, ]) { final paint = Paint() ..color = colorScheme?.primary ?? Colors.black diff --git a/app/lib/helpers/boolean_ops.dart b/app/lib/helpers/boolean_ops.dart new file mode 100644 index 000000000000..204bf72d7dec --- /dev/null +++ b/app/lib/helpers/boolean_ops.dart @@ -0,0 +1,205 @@ +import 'dart:ui'; +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:butterfly/helpers/point.dart'; +import 'package:dart_leap/dart_leap.dart'; + +Path? elementToPath(PadElement element) { + if (element is ShapeElement) { + final rect = Rect.fromPoints( + element.firstPosition.toOffset(), + element.secondPosition.toOffset(), + ); + final shape = element.property.shape; + final drawRect = rect.inflate(-element.property.strokeWidth / 2); + + if (shape is RectangleShape) { + final topLeft = shape.topLeftCornerRadius / 100 * drawRect.shortestSide; + final topRight = shape.topRightCornerRadius / 100 * drawRect.shortestSide; + final bottomLeft = + shape.bottomLeftCornerRadius / 100 * drawRect.shortestSide; + final bottomRight = + shape.bottomRightCornerRadius / 100 * drawRect.shortestSide; + return Path()..addRRect( + RRect.fromRectAndCorners( + drawRect, + topLeft: Radius.circular(topLeft), + topRight: Radius.circular(topRight), + bottomLeft: Radius.circular(bottomLeft), + bottomRight: Radius.circular(bottomRight), + ), + ); + } else if (shape is CircleShape) { + return Path()..addOval(drawRect); + } else if (shape is TriangleShape) { + final topCenter = drawRect.topCenter; + return Path() + ..moveTo(topCenter.dx, topCenter.dy) + ..lineTo(drawRect.right, drawRect.bottom) + ..lineTo(drawRect.left, drawRect.bottom) + ..close(); + } else if (shape is LineShape) { + // Lines don't have area for boolean ops usually, unless stroked. + // But for now let's ignore or treat as thin rectangle? + // Treating as path is fine, but combine might fail or do nothing if it has no area. + return Path() + ..moveTo(element.firstPosition.x, element.firstPosition.y) + ..lineTo(element.secondPosition.x, element.secondPosition.y); + } + } else if (element is PolygonElement) { + final points = element.points; + if (points.isEmpty) return null; + final path = Path(); + final first = points.first; + path.moveTo(first.x, first.y); + for (var i = 1; i < points.length; i++) { + final point = points[i]; + final prev = points[i - 1]; + + if (prev.handleOut != null || point.handleIn != null) { + path.cubicTo( + prev.handleOut?.x ?? prev.x, + prev.handleOut?.y ?? prev.y, + point.handleIn?.x ?? point.x, + point.handleIn?.y ?? point.y, + point.x, + point.y, + ); + } else { + path.lineTo(point.x, point.y); + } + } + path.close(); // Polygons should be closed for boolean ops + return path; + } + return null; +} + +List performPathOperation( + List elements, + PathOperation operation, +) { + if (elements.isEmpty) return []; + var currentPath = elementToPath(elements.first); + if (currentPath == null) return []; + + for (var i = 1; i < elements.length; i++) { + final nextPath = elementToPath(elements[i]); + if (nextPath != null) { + currentPath = Path.combine(operation, currentPath!, nextPath); + } + } + + if (currentPath == null) return []; + + // Convert back to polygons + final polygons = []; + final metrics = currentPath.computeMetrics(); + + for (final metric in metrics) { + final points = []; + // Sample points + final length = metric.length; + final startTangent = metric.getTangentForOffset(0); + if (startTangent != null) { + _addPoint(points, startTangent.position); + final endTangent = metric.getTangentForOffset(length); + if (endTangent != null) { + _adaptiveSample( + metric, + 0, + length, + startTangent.position, + endTangent.position, + points, + ); + } + } + + if (points.length > 2) { + // Copy properties from the first element or use default + final first = elements.first; + ShapeProperty? shapeProp; + PolygonProperty? polyProp; + + if (first is ShapeElement) shapeProp = first.property; + if (first is PolygonElement) polyProp = first.property; + + final newProp = + polyProp ?? + (shapeProp != null + ? PolygonProperty( + strokeWidth: shapeProp.strokeWidth, + color: shapeProp.color, + fill: shapeProp.shape is! LineShape + ? (shapeProp.shape as dynamic).fillColor + : SRGBColor.transparent, + ) + : const PolygonProperty()); + + polygons.add(PolygonElement(points: points, property: newProp)); + } + } + return polygons; +} + +bool _isFlat(Offset p1, Offset p2, Offset p3) { + final d1 = (p2 - p1).distance; + final d2 = (p3 - p2).distance; + final d3 = (p3 - p1).distance; + return (d1 + d2 - d3).abs() < 0.05; +} + +void _addPoint(List points, Offset pos) { + final x = pos.dx; + final y = pos.dy; + + if (points.isNotEmpty) { + final last = points.last; + if ((x - last.x).abs() < 0.05 && (y - last.y).abs() < 0.05) { + return; + } + + if (points.length >= 2) { + final secondLast = points[points.length - 2]; + final dx1 = last.x - secondLast.x; + final dy1 = last.y - secondLast.y; + final dx2 = x - last.x; + final dy2 = y - last.y; + + final cross = dx1 * dy2 - dy1 * dx2; + if (cross.abs() < 0.1) { + points.removeLast(); + } + } + } + points.add(PolygonPoint(x, y)); +} + +void _adaptiveSample( + PathMetric metric, + double start, + double end, + Offset pStart, + Offset pEnd, + List points, +) { + if (end - start < 0.5) { + _addPoint(points, pEnd); + return; + } + + final mid = (start + end) / 2; + final tMid = metric.getTangentForOffset(mid); + if (tMid == null) { + _addPoint(points, pEnd); + return; + } + final pMid = tMid.position; + + if (_isFlat(pStart, pMid, pEnd)) { + _addPoint(points, pEnd); + } else { + _adaptiveSample(metric, start, mid, pStart, pMid, points); + _adaptiveSample(metric, mid, end, pMid, pEnd, points); + } +} diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index b5d8d3679cfd..929ceca2d36d 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -1291,5 +1291,21 @@ "redLinedRuledDark": "Red lined ruled dark", "redLinedQuad": "Red lined quad", "redLinedQuadDark": "Red lined quad dark", - "noItemSelected": "No item selected" + "noItemSelected": "No item selected", + "union": "Union", + "@union": { + "description": "Union boolean operation" + }, + "difference": "Difference", + "@difference": { + "description": "Difference boolean operation" + }, + "intersect": "Intersect", + "@intersect": { + "description": "Intersect boolean operation" + }, + "xor": "XOR", + "@xor": { + "description": "XOR boolean operation" + } } diff --git a/app/lib/renderers/backgrounds/image.dart b/app/lib/renderers/backgrounds/image.dart index d25fd6eae1ff..f583b0b7a475 100644 --- a/app/lib/renderers/backgrounds/image.dart +++ b/app/lib/renderers/backgrounds/image.dart @@ -15,6 +15,7 @@ class ImageBackgroundRenderer extends Renderer { CameraTransform transform, [ ColorScheme? colorScheme, bool foreground = false, + bool wireframeMode = false, ]) { if (image == null) return; final sizeX = element.width * element.scaleX * transform.size; diff --git a/app/lib/renderers/backgrounds/svg.dart b/app/lib/renderers/backgrounds/svg.dart index 3ac8ad07d47d..9fb9acacd3a6 100644 --- a/app/lib/renderers/backgrounds/svg.dart +++ b/app/lib/renderers/backgrounds/svg.dart @@ -29,6 +29,7 @@ class SvgBackgroundRenderer extends Renderer { CameraTransform transform, [ ColorScheme? colorScheme, bool foreground = false, + bool wireframeMode = false, ]) { if (_pictureInfo == null) return; final sizeX = element.width * element.scaleX * transform.size; diff --git a/app/lib/renderers/backgrounds/texture.dart b/app/lib/renderers/backgrounds/texture.dart index d3139cf6760f..e1ec32253eec 100644 --- a/app/lib/renderers/backgrounds/texture.dart +++ b/app/lib/renderers/backgrounds/texture.dart @@ -15,6 +15,7 @@ class TextureBackgroundRenderer extends Renderer { CameraTransform transform, [ ColorScheme? colorScheme, bool foreground = false, + bool wireframeMode = false, ]) => drawSurfaceTextureOnCanvas( texture, canvas, diff --git a/app/lib/renderers/cursors/eraser.dart b/app/lib/renderers/cursors/eraser.dart index a8efa77e8e49..8984c9610f6a 100644 --- a/app/lib/renderers/cursors/eraser.dart +++ b/app/lib/renderers/cursors/eraser.dart @@ -28,6 +28,7 @@ class EraserCursor extends Renderer> { CameraTransform transform, [ ColorScheme? colorScheme, bool foreground = false, + bool wireframeMode = false, ]) { final radius = element.tool.strokeWidth / 2; final position = transform.localToGlobal(element.position); diff --git a/app/lib/renderers/cursors/label.dart b/app/lib/renderers/cursors/label.dart index 4dc7a727cd62..a46087fe8886 100644 --- a/app/lib/renderers/cursors/label.dart +++ b/app/lib/renderers/cursors/label.dart @@ -29,6 +29,7 @@ class LabelCursor extends Renderer { CameraTransform transform, [ ColorScheme? colorScheme, bool foreground = false, + bool wireframeMode = false, ]) { const icon = PhosphorIconsLight.cursorText; final property = switch (element.context) { @@ -80,6 +81,7 @@ class LabelSelectionCursor extends Renderer { CameraTransform transform, [ ColorScheme? colorScheme, bool foreground = false, + bool wireframeMode = false, ]) { final color = colorScheme?.primary ?? Colors.blue; // Paint vertical line diff --git a/app/lib/renderers/cursors/user.dart b/app/lib/renderers/cursors/user.dart index 1ef0ff3453f3..5db6bd34b8e2 100644 --- a/app/lib/renderers/cursors/user.dart +++ b/app/lib/renderers/cursors/user.dart @@ -23,6 +23,7 @@ class UserCursor extends Renderer { CameraTransform transform, [ ColorScheme? colorScheme, bool foreground = false, + bool wireframeMode = false, ]) { final position = element.cursor?.toOffset(); if (position == null) { diff --git a/app/lib/renderers/elements/image.dart b/app/lib/renderers/elements/image.dart index 3dbbb8134160..8788d3ac23b5 100644 --- a/app/lib/renderers/elements/image.dart +++ b/app/lib/renderers/elements/image.dart @@ -40,7 +40,16 @@ class ImageRenderer extends Renderer { CameraTransform transform, [ ColorScheme? colorScheme, bool foreground = false, + bool wireframeMode = false, ]) { + if (wireframeMode) { + final paint = Paint() + ..color = Colors.blue + ..style = PaintingStyle.stroke + ..strokeWidth = 2 / transform.size; + canvas.drawRect(rect, paint); + return; + } if (image == null) { // Render placeholder final paint = Paint() diff --git a/app/lib/renderers/elements/pdf.dart b/app/lib/renderers/elements/pdf.dart index 8e194f1a014e..ad3398f4eec7 100644 --- a/app/lib/renderers/elements/pdf.dart +++ b/app/lib/renderers/elements/pdf.dart @@ -36,7 +36,16 @@ class PdfRenderer extends Renderer { CameraTransform transform, [ ColorScheme? colorScheme, bool foreground = false, + bool wireframeMode = false, ]) { + if (wireframeMode) { + final paint = Paint() + ..color = Colors.blue + ..style = PaintingStyle.stroke + ..strokeWidth = 2 / transform.size; + canvas.drawRect(rect, paint); + return; + } if (this.image == null || element.background.a > 0) { // Render placeholder final paint = Paint() diff --git a/app/lib/renderers/elements/pen.dart b/app/lib/renderers/elements/pen.dart index 7a69b434badb..d83f3d1b03f9 100644 --- a/app/lib/renderers/elements/pen.dart +++ b/app/lib/renderers/elements/pen.dart @@ -128,6 +128,7 @@ class PenRenderer extends Renderer { CameraTransform transform, [ ColorScheme? colorScheme, bool foreground = false, + bool wireframeMode = false, ]) { final points = element.points; if (points.isEmpty) return; @@ -137,18 +138,23 @@ class PenRenderer extends Renderer { _computePaths(); } - if (property.fill.a > 0 && _cachedFillPath != null) { + if (property.fill.a > 0 && _cachedFillPath != null && !wireframeMode) { final paint = Paint() ..color = property.fill.toColor() ..style = PaintingStyle.fill ..strokeCap = StrokeCap.round; canvas.drawPath(_cachedFillPath!, paint); } - if (property.color.a > 0 && _cachedStrokePath != null) { + if ((property.color.a > 0 || wireframeMode) && _cachedStrokePath != null) { final paint = Paint() ..color = property.color.toColor() - ..style = PaintingStyle.fill + ..style = PaintingStyle.fill // Pen stroke is a filled path ..strokeCap = StrokeCap.round; + if (wireframeMode) { + paint.style = PaintingStyle.stroke; + paint.strokeWidth = 1 / transform.size; + paint.color = Colors.black; // Ensure visibility in wireframe + } canvas.drawPath(_cachedStrokePath!, paint); } } diff --git a/app/lib/renderers/elements/polygon.dart b/app/lib/renderers/elements/polygon.dart index e0f148a2bda6..381202dbec56 100644 --- a/app/lib/renderers/elements/polygon.dart +++ b/app/lib/renderers/elements/polygon.dart @@ -84,6 +84,7 @@ class PolygonRenderer extends Renderer { CameraTransform transform, [ ColorScheme? colorScheme, bool foreground = false, + bool wireframeMode = false, ]) { if (element.points.isEmpty) return; @@ -96,7 +97,7 @@ class PolygonRenderer extends Renderer { final property = element.property; - if (property.color.a > 0) { + if (property.color.a > 0 || wireframeMode) { final paint = Paint() ..color = property.color.toColor() ..style = PaintingStyle.stroke @@ -105,7 +106,7 @@ class PolygonRenderer extends Renderer { ..strokeJoin = StrokeJoin.round; canvas.drawPath(path, paint); } - if (property.fill.a > 0) { + if (property.fill.a > 0 && !wireframeMode) { final fillPaint = Paint() ..color = property.fill.toColor() ..style = PaintingStyle.fill; diff --git a/app/lib/renderers/elements/shape.dart b/app/lib/renderers/elements/shape.dart index 6d39dca381d9..74e3b3b183a7 100644 --- a/app/lib/renderers/elements/shape.dart +++ b/app/lib/renderers/elements/shape.dart @@ -58,6 +58,7 @@ class ShapeRenderer extends Renderer { CameraTransform transform, [ ColorScheme? colorScheme, bool foreground = false, + bool wireframeMode = false, ]) { final shape = element.property.shape; final strokeWidth = element.property.strokeWidth; @@ -77,20 +78,22 @@ class ShapeRenderer extends Renderer { final bottomRightCornerRadius = Radius.circular( shape.bottomRightCornerRadius / 100 * drawRect.shortestSide, ); - canvas.drawRRect( - RRect.fromRectAndCorners( - drawRect, - topLeft: topLeftCornerRadius, - topRight: topRightCornerRadius, - bottomLeft: bottomLeftCornerRadius, - bottomRight: bottomRightCornerRadius, - ), - _buildPaint( - color: shape.fillColor.toColor(), - style: PaintingStyle.fill, - ), - ); - if (strokeWidth > 0) { + if (!wireframeMode) { + canvas.drawRRect( + RRect.fromRectAndCorners( + drawRect, + topLeft: topLeftCornerRadius, + topRight: topRightCornerRadius, + bottomLeft: bottomLeftCornerRadius, + bottomRight: bottomRightCornerRadius, + ), + _buildPaint( + color: shape.fillColor.toColor(), + style: PaintingStyle.fill, + ), + ); + } + if (strokeWidth > 0 || wireframeMode) { final rrect = RRect.fromRectAndCorners( drawRect, topLeft: topLeftCornerRadius, @@ -102,14 +105,16 @@ class ShapeRenderer extends Renderer { _drawStyledPath(canvas, path, paint); } } else if (shape is CircleShape) { - canvas.drawOval( - drawRect, - _buildPaint( - color: shape.fillColor.toColor(), - style: PaintingStyle.fill, - ), - ); - if (strokeWidth > 0) { + if (!wireframeMode) { + canvas.drawOval( + drawRect, + _buildPaint( + color: shape.fillColor.toColor(), + style: PaintingStyle.fill, + ), + ); + } + if (strokeWidth > 0 || wireframeMode) { final path = Path()..addOval(drawRect); _drawStyledPath(canvas, path, paint); } @@ -125,14 +130,16 @@ class ShapeRenderer extends Renderer { ..lineTo(drawRect.right, drawRect.bottom) ..lineTo(drawRect.left, drawRect.bottom) ..close(); - canvas.drawPath( - path, - _buildPaint( - color: shape.fillColor.toColor(), - style: PaintingStyle.fill, - ), - ); - if (strokeWidth > 0) { + if (!wireframeMode) { + canvas.drawPath( + path, + _buildPaint( + color: shape.fillColor.toColor(), + style: PaintingStyle.fill, + ), + ); + } + if (strokeWidth > 0 || wireframeMode) { _drawStyledPath(canvas, path, paint); } } diff --git a/app/lib/renderers/elements/svg.dart b/app/lib/renderers/elements/svg.dart index 88e7d65f4a77..217811a3f0b7 100644 --- a/app/lib/renderers/elements/svg.dart +++ b/app/lib/renderers/elements/svg.dart @@ -15,8 +15,17 @@ class SvgRenderer extends Renderer { CameraTransform transform, [ ColorScheme? colorScheme, bool foreground = false, + bool wireframeMode = false, ]) { final rect = this.rect; + if (wireframeMode) { + final paint = Paint() + ..color = Colors.blue + ..style = PaintingStyle.stroke + ..strokeWidth = 2 / transform.size; + canvas.drawRect(rect, paint); + return; + } if (pictureInfo == null) { // Render placeholder final paint = Paint() diff --git a/app/lib/renderers/elements/text.dart b/app/lib/renderers/elements/text.dart index a9304dc428af..ff1d9d749d89 100644 --- a/app/lib/renderers/elements/text.dart +++ b/app/lib/renderers/elements/text.dart @@ -171,7 +171,16 @@ abstract class GenericTextRenderer extends Renderer { CameraTransform transform, [ ColorScheme? colorScheme, bool foreground = false, + bool wireframeMode = false, ]) { + if (wireframeMode) { + final paint = Paint() + ..color = Colors.blue + ..style = PaintingStyle.stroke + ..strokeWidth = 2 / transform.size; + canvas.drawRect(rect, paint); + return; + } final tp = _tp; if (tp == null || tp.text == null) return; tp.layout(maxWidth: rect.width); diff --git a/app/lib/renderers/elements/texture.dart b/app/lib/renderers/elements/texture.dart index 41161adbe87e..a1b3a4fe44d6 100644 --- a/app/lib/renderers/elements/texture.dart +++ b/app/lib/renderers/elements/texture.dart @@ -19,7 +19,17 @@ class TextureRenderer extends Renderer { CameraTransform transform, [ ColorScheme? colorScheme, bool foreground = false, - ]) => drawSurfaceTextureOnCanvas( + bool wireframeMode = false, + ]) { + if (wireframeMode) { + final paint = Paint() + ..color = Colors.blue + ..style = PaintingStyle.stroke + ..strokeWidth = 2 / transform.size; + canvas.drawRect(rect, paint); + return; + } + drawSurfaceTextureOnCanvas( element.texture, canvas, 1, @@ -27,6 +37,7 @@ class TextureRenderer extends Renderer { rect.size, rect.topLeft, ); + } @override void buildSvg( diff --git a/app/lib/renderers/foregrounds/area.dart b/app/lib/renderers/foregrounds/area.dart index ef0d2b21beb0..860074799338 100644 --- a/app/lib/renderers/foregrounds/area.dart +++ b/app/lib/renderers/foregrounds/area.dart @@ -16,6 +16,7 @@ class AreaForegroundRenderer extends Renderer { CameraTransform transform, [ ColorScheme? colorScheme, bool foreground = false, + bool wireframeMode = false, ]) { final paint = Paint() ..style = PaintingStyle.stroke diff --git a/app/lib/renderers/foregrounds/select.dart b/app/lib/renderers/foregrounds/select.dart index 7aa12bf131d6..681b35eef11b 100644 --- a/app/lib/renderers/foregrounds/select.dart +++ b/app/lib/renderers/foregrounds/select.dart @@ -24,6 +24,7 @@ class LassoSelectionForegroundRenderer extends Renderer> { CameraTransform transform, [ ColorScheme? colorScheme, bool foreground = false, + bool wireframeMode = false, ]) { final paint = Paint() ..color = scheme.primaryContainer @@ -269,6 +270,7 @@ class RectSelectionForegroundRenderer extends Renderer { CameraTransform transform, [ ColorScheme? colorScheme, bool foreground = false, + bool wireframeMode = false, ]) { if (element.isEmpty || isTransforming) return; final paint = Paint() diff --git a/app/lib/renderers/renderer.dart b/app/lib/renderers/renderer.dart index 2a482bb99f7b..6c5d8c3d64f5 100644 --- a/app/lib/renderers/renderer.dart +++ b/app/lib/renderers/renderer.dart @@ -328,6 +328,7 @@ abstract class Renderer { CameraTransform transform, [ ColorScheme? colorScheme, bool foreground = false, + bool wireframeMode = false, ]); HitCalculator getHitCalculator() => diff --git a/app/lib/selections/elements/element.dart b/app/lib/selections/elements/element.dart index 13d4e9e5c3cf..7f911a95d1bb 100644 --- a/app/lib/selections/elements/element.dart +++ b/app/lib/selections/elements/element.dart @@ -46,8 +46,44 @@ class ElementSelection extends Selection> { (p, e) => p + (e.rect?.topLeft ?? Offset.zero), ) / selected.length.toDouble(); + final canPerformBooleanOp = + elements.length > 1 && + elements.every((e) => e is ShapeElement || e is PolygonElement); return [ ...super.buildProperties(context), + if (canPerformBooleanOp) + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + onPressed: () => + _performBooleanOp(context, ui.PathOperation.union), + icon: const PhosphorIcon(PhosphorIconsLight.squaresFour), + tooltip: AppLocalizations.of(context).union, + ), + IconButton( + onPressed: () => + _performBooleanOp(context, ui.PathOperation.difference), + icon: const PhosphorIcon(PhosphorIconsLight.subtract), + tooltip: AppLocalizations.of(context).difference, + ), + IconButton( + onPressed: () => + _performBooleanOp(context, ui.PathOperation.intersect), + icon: const PhosphorIcon(PhosphorIconsLight.intersect), + tooltip: AppLocalizations.of(context).intersect, + ), + IconButton( + onPressed: () => + _performBooleanOp(context, ui.PathOperation.xor), + icon: const PhosphorIcon(PhosphorIconsLight.exclude), + tooltip: AppLocalizations.of(context).xor, + ), + ], + ), + ), OffsetListTile( value: position, title: Text(AppLocalizations.of(context).position), @@ -180,6 +216,17 @@ class ElementSelection extends Selection> { return selection; } + void _performBooleanOp(BuildContext context, ui.PathOperation op) { + final elements = this.elements; + final newElements = performPathOperation(elements, op); + if (newElements.isNotEmpty) { + final bloc = context.read(); + final idsToRemove = elements.map((e) => e.id!).toList(); + bloc.add(ElementsRemoved(idsToRemove)); + bloc.add(ElementsCreated(newElements)); + } + } + @override String getLocalizedName(BuildContext context) => AppLocalizations.of(context).element; diff --git a/app/lib/selections/selection.dart b/app/lib/selections/selection.dart index 45a228e361e6..1695eca67c8d 100644 --- a/app/lib/selections/selection.dart +++ b/app/lib/selections/selection.dart @@ -10,6 +10,8 @@ import 'package:butterfly/visualizer/preset.dart'; import 'package:butterfly/visualizer/property.dart'; import 'package:butterfly/widgets/color_field.dart'; import 'package:butterfly/src/generated/i18n/app_localizations.dart'; +import 'package:butterfly/helpers/boolean_ops.dart'; +import 'dart:ui' as ui; import 'package:butterfly_api/butterfly_api.dart'; import 'package:flutter/material.dart'; diff --git a/app/lib/view_painter.dart b/app/lib/view_painter.dart index 5a7c268fafeb..2d99eb2d90c8 100644 --- a/app/lib/view_painter.dart +++ b/app/lib/view_painter.dart @@ -21,6 +21,7 @@ class ForegroundPainter extends CustomPainter { final CameraTransform transform; final Selection? selection; final NavigatorPosition navigatorPosition; + final bool wireframeMode; ForegroundPainter( this.renderers, @@ -31,6 +32,7 @@ class ForegroundPainter extends CustomPainter { this.transform = const CameraTransform(), this.selection, this.navigatorPosition = NavigatorPosition.left, + this.wireframeMode = false, ]); @override @@ -58,6 +60,7 @@ class ForegroundPainter extends CustomPainter { transform, colorScheme, true, + wireframeMode, ); if (center != null) { canvas.translate(center.dx, center.dy); @@ -99,7 +102,8 @@ class ForegroundPainter extends CustomPainter { oldDelegate.renderers != renderers || oldDelegate.transform != transform || oldDelegate.selection != selection || - oldDelegate.colorScheme != colorScheme; + oldDelegate.colorScheme != colorScheme || + oldDelegate.wireframeMode != wireframeMode; } class ViewPainter extends CustomPainter { @@ -112,6 +116,7 @@ class ViewPainter extends CustomPainter { final CameraTransform transform; final ColorScheme? colorScheme; final Set? invisibleLayers; + final bool wireframeMode; const ViewPainter( this.document, @@ -125,6 +130,7 @@ class ViewPainter extends CustomPainter { required this.cameraViewport, this.colorScheme, this.transform = const CameraTransform(), + this.wireframeMode = false, }); @override @@ -213,6 +219,7 @@ class ViewPainter extends CustomPainter { transform, colorScheme, false, + wireframeMode, ); if (center != null) { canvas.translate(center.dx, center.dy); @@ -247,7 +254,8 @@ class ViewPainter extends CustomPainter { renderBackground != oldDelegate.renderBackground || transform != oldDelegate.transform || cameraViewport != oldDelegate.cameraViewport || - colorScheme != oldDelegate.colorScheme; + colorScheme != oldDelegate.colorScheme || + wireframeMode != oldDelegate.wireframeMode; return shouldRepaint; } } diff --git a/app/lib/views/app_bar.dart b/app/lib/views/app_bar.dart index c2c12c8772f5..9fe025398480 100644 --- a/app/lib/views/app_bar.dart +++ b/app/lib/views/app_bar.dart @@ -690,6 +690,14 @@ class _MainPopupMenu extends StatelessWidget { }, child: Text(AppLocalizations.of(context).hideUI), ), + MenuItemButton( + leadingIcon: state.wireframeMode + ? const PhosphorIcon(PhosphorIconsFill.frameCorners) + : const PhosphorIcon(PhosphorIconsLight.frameCorners), + onPressed: () => + context.read().toggleWireframeMode(), + child: const Text('Wireframe Mode'), + ), BlocBuilder( buildWhen: (previous, current) => previous.fullScreen != current.fullScreen, diff --git a/app/lib/views/view.dart b/app/lib/views/view.dart index faf519bf970e..6baafef1d29c 100644 --- a/app/lib/views/view.dart +++ b/app/lib/views/view.dart @@ -580,6 +580,7 @@ class _MainViewViewportState extends State frictionTransform, cubit.state.selection, state.settingsCubit.state.navigatorPosition, + currentIndex.wireframeMode, ), painter: ViewPainter( state.data, @@ -590,6 +591,7 @@ class _MainViewViewportState extends State invisibleLayers: state.invisibleLayers, currentArea: state.currentArea, colorScheme: ColorScheme.of(context), + wireframeMode: currentIndex.wireframeMode, ), isComplex: true, willChange: true,