mirror of
https://github.com/LinwoodDev/Butterfly.git
synced 2025-08-14 17:31:20 +08:00
Implement arrangement
This commit is contained in:
@ -3,7 +3,7 @@
|
||||
/// More dartdocs go here.
|
||||
library butterfly_api;
|
||||
|
||||
export 'src/butterfly_models.dart';
|
||||
export 'src/butterfly_helpers.dart';
|
||||
export 'butterfly_models.dart';
|
||||
export 'butterfly_helpers.dart';
|
||||
|
||||
// TODO: Export any libraries intended for clients of this package.
|
||||
|
3
api/lib/butterfly_helpers.dart
Normal file
3
api/lib/butterfly_helpers.dart
Normal file
@ -0,0 +1,3 @@
|
||||
export 'src/helpers/asset_helper.dart';
|
||||
export 'src/helpers/point_helper.dart';
|
||||
export 'src/helpers/search_helper.dart';
|
22
api/lib/butterfly_models.dart
Normal file
22
api/lib/butterfly_models.dart
Normal file
@ -0,0 +1,22 @@
|
||||
export 'src/converter/core.dart';
|
||||
export 'src/converter/legacy.dart';
|
||||
export 'src/converter/note.dart';
|
||||
export 'src/models/animation.dart';
|
||||
export 'src/models/archive.dart';
|
||||
export 'src/models/area.dart';
|
||||
export 'src/models/asset.dart';
|
||||
export 'src/models/background.dart';
|
||||
export 'src/models/colors.dart';
|
||||
export 'src/models/data.dart';
|
||||
export 'src/models/element.dart';
|
||||
export 'src/models/export.dart';
|
||||
export 'src/models/info.dart';
|
||||
export 'src/models/meta.dart';
|
||||
export 'src/models/pack.dart';
|
||||
export 'src/models/page.dart';
|
||||
export 'src/models/painter.dart';
|
||||
export 'src/models/palette.dart';
|
||||
export 'src/models/point.dart';
|
||||
export 'src/models/property.dart';
|
||||
export 'src/models/tool.dart';
|
||||
export 'src/models/waypoint.dart';
|
@ -1,3 +0,0 @@
|
||||
export 'helpers/asset_helper.dart';
|
||||
export 'helpers/point_helper.dart';
|
||||
export 'helpers/search_helper.dart';
|
@ -1,22 +0,0 @@
|
||||
export 'converter/core.dart';
|
||||
export 'converter/legacy.dart';
|
||||
export 'converter/note.dart';
|
||||
export 'models/animation.dart';
|
||||
export 'models/archive.dart';
|
||||
export 'models/area.dart';
|
||||
export 'models/asset.dart';
|
||||
export 'models/background.dart';
|
||||
export 'models/colors.dart';
|
||||
export 'models/data.dart';
|
||||
export 'models/element.dart';
|
||||
export 'models/export.dart';
|
||||
export 'models/info.dart';
|
||||
export 'models/meta.dart';
|
||||
export 'models/pack.dart';
|
||||
export 'models/page.dart';
|
||||
export 'models/painter.dart';
|
||||
export 'models/palette.dart';
|
||||
export 'models/point.dart';
|
||||
export 'models/property.dart';
|
||||
export 'models/tool.dart';
|
||||
export 'models/waypoint.dart';
|
@ -1,10 +1,11 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:butterfly_api/src/butterfly_helpers.dart';
|
||||
import 'package:butterfly_api/src/models/data.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import '../helpers/asset_helper.dart';
|
||||
import 'data.dart';
|
||||
|
||||
part 'asset.freezed.dart';
|
||||
part 'asset.g.dart';
|
||||
|
||||
|
@ -174,41 +174,95 @@ class DocumentBloc extends ReplayBloc<DocumentEvent, DocumentState> {
|
||||
null);
|
||||
}
|
||||
}, transformer: sequential());
|
||||
on<ElementsRemoved>((event, emit) async {
|
||||
if (state is DocumentLoadSuccess) {
|
||||
final current = state as DocumentLoadSuccess;
|
||||
if (!(current.embedding?.editable ?? true)) return;
|
||||
if (event.elements.isEmpty ||
|
||||
!current.page.content
|
||||
.any((element) => event.elements.contains(element))) return;
|
||||
final page = current.page;
|
||||
final renderers = current.renderers;
|
||||
current.currentIndexCubit.unbake(
|
||||
unbakedElements: renderers.where((element) {
|
||||
final remaining = !event.elements.contains(
|
||||
element.element,
|
||||
);
|
||||
if (!remaining) element.dispose();
|
||||
return remaining;
|
||||
}).toList(),
|
||||
);
|
||||
final newPage = page.copyWith(
|
||||
content: List.from(page.content)
|
||||
..removeWhere((element) => event.elements.contains(element)));
|
||||
// Remove unused assets
|
||||
final unusedAssets = <String>{};
|
||||
event.elements.whereType<SourcedElement>().forEach((element) {
|
||||
final uri = Uri.tryParse(element.source);
|
||||
if (uri?.scheme == '' && !newPage.usesSource(element.source)) {
|
||||
unusedAssets.add(element.source);
|
||||
}
|
||||
});
|
||||
for (var asset in unusedAssets) {
|
||||
current.data.removeAsset(asset);
|
||||
on<ElementsArranged>((event, emit) async {
|
||||
final current = state;
|
||||
if (current is! DocumentLoadSuccess) return;
|
||||
final renderers = await Future.wait(event.elements.map((e) async {
|
||||
final renderer = Renderer.fromInstance(e);
|
||||
await renderer.setup(current.data, current.assetService, current.page);
|
||||
return renderer;
|
||||
}).toList());
|
||||
var content = List<PadElement>.from(current.page.content);
|
||||
final transform = current.transformCubit.state;
|
||||
for (var renderer in renderers) {
|
||||
final index = content.indexOf(renderer.element);
|
||||
if (index == -1) {
|
||||
content.add(renderer.element);
|
||||
continue;
|
||||
}
|
||||
content.removeAt(index);
|
||||
var newIndex = index;
|
||||
if (event.arrangement == Arrangement.front) {
|
||||
newIndex = content.length - 1;
|
||||
} else if (event.arrangement == Arrangement.back) {
|
||||
newIndex = 0;
|
||||
} else {
|
||||
final rect = renderer.rect;
|
||||
if (rect != null) {
|
||||
final hits = (await rayCastRect(rect, this, transform))
|
||||
.map((e) => e.element)
|
||||
.toList();
|
||||
final hitIndex = hits.indexOf(renderer.element);
|
||||
if (hitIndex != -1) {
|
||||
if (event.arrangement == Arrangement.backward && hitIndex != 0) {
|
||||
newIndex = content.indexOf(hits[hitIndex - 1]);
|
||||
} else if (event.arrangement == Arrangement.forward &&
|
||||
hitIndex != hits.length - 1) {
|
||||
newIndex = content.indexOf(hits[hitIndex + 1]) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newIndex >= 0) {
|
||||
content.insert(newIndex, renderer.element);
|
||||
} else {
|
||||
content.add(renderer.element);
|
||||
}
|
||||
|
||||
await _saveState(emit, current.copyWith(page: newPage), null);
|
||||
}
|
||||
final newPage = current.page.copyWith(content: content);
|
||||
return _saveState(
|
||||
emit,
|
||||
current.copyWith(
|
||||
page: newPage,
|
||||
),
|
||||
null)
|
||||
.whenComplete(() => current.currentIndexCubit
|
||||
.loadElements(current.data, current.assetService, newPage));
|
||||
});
|
||||
on<ElementsRemoved>((event, emit) async {
|
||||
final current = state;
|
||||
if (current is! DocumentLoadSuccess) return;
|
||||
if (!(current.embedding?.editable ?? true)) return;
|
||||
if (event.elements.isEmpty ||
|
||||
!current.page.content
|
||||
.any((element) => event.elements.contains(element))) return;
|
||||
final page = current.page;
|
||||
final renderers = current.renderers;
|
||||
current.currentIndexCubit.unbake(
|
||||
unbakedElements: renderers.where((element) {
|
||||
final remaining = !event.elements.contains(
|
||||
element.element,
|
||||
);
|
||||
if (!remaining) element.dispose();
|
||||
return remaining;
|
||||
}).toList(),
|
||||
);
|
||||
final newPage = page.copyWith(
|
||||
content: List.from(page.content)
|
||||
..removeWhere((element) => event.elements.contains(element)));
|
||||
// Remove unused assets
|
||||
final unusedAssets = <String>{};
|
||||
event.elements.whereType<SourcedElement>().forEach((element) {
|
||||
final uri = Uri.tryParse(element.source);
|
||||
if (uri?.scheme == '' && !newPage.usesSource(element.source)) {
|
||||
unusedAssets.add(element.source);
|
||||
}
|
||||
});
|
||||
for (var asset in unusedAssets) {
|
||||
current.data.removeAsset(asset);
|
||||
}
|
||||
|
||||
await _saveState(emit, current.copyWith(page: newPage), null);
|
||||
}, transformer: sequential());
|
||||
on<DocumentDescriptorChanged>((event, emit) async {
|
||||
if (state is DocumentLoadSuccess) {
|
||||
|
@ -64,15 +64,15 @@ class ElementsRemoved extends DocumentEvent {
|
||||
List<Object?> get props => [elements];
|
||||
}
|
||||
|
||||
enum Arangement { forward, backward, front, back }
|
||||
enum Arrangement { forward, backward, front, back }
|
||||
|
||||
class ElementsArranged extends DocumentEvent {
|
||||
final List<PadElement> elements;
|
||||
final Arangement arangement;
|
||||
final Arrangement arrangement;
|
||||
|
||||
const ElementsArranged(this.elements, this.arangement);
|
||||
const ElementsArranged(this.elements, this.arrangement);
|
||||
@override
|
||||
List<Object?> get props => [elements, arangement];
|
||||
List<Object?> get props => [elements, arrangement];
|
||||
}
|
||||
|
||||
class DocumentDescriptorChanged extends DocumentEvent {
|
||||
|
@ -450,6 +450,12 @@ class CurrentIndexCubit extends Cubit<CurrentIndex> {
|
||||
|
||||
Future<void> loadElements(
|
||||
NoteData document, AssetService assetService, DocumentPage page) async {
|
||||
for (var e in state.cameraViewport.unbakedElements) {
|
||||
e.dispose();
|
||||
}
|
||||
for (var e in state.cameraViewport.bakedElements) {
|
||||
e.dispose();
|
||||
}
|
||||
final renderers =
|
||||
page.content.map((e) => Renderer.fromInstance(e)).toList();
|
||||
await Future.wait(renderers
|
||||
|
@ -75,7 +75,7 @@ class ElementsDialog extends StatelessWidget {
|
||||
SubmenuButton(
|
||||
leadingIcon: const Icon(PhosphorIconsLight.layout),
|
||||
menuStyle: const MenuStyle(alignment: Alignment.centerRight),
|
||||
menuChildren: Arangement.values
|
||||
menuChildren: Arrangement.values
|
||||
.map((e) => MenuItemButton(
|
||||
leadingIcon: Icon(e.icon(PhosphorIconsStyle.light)),
|
||||
child: Text(e.getLocalizedName(context)),
|
||||
|
@ -40,7 +40,8 @@ class EraserHandler extends Handler<EraserPainter> {
|
||||
if (!_currentlyErasing) {
|
||||
_currentlyErasing = true;
|
||||
// Raycast
|
||||
final ray = await rayCast(globalPos, context.buildContext, size);
|
||||
final ray = await rayCast(globalPos, context.getDocumentBloc(),
|
||||
context.getCameraTransform(), size);
|
||||
final newElements = ray
|
||||
.map((e) => e.element)
|
||||
.whereType<PenElement>()
|
||||
|
@ -285,7 +285,8 @@ class HandHandler extends Handler<HandPainter> {
|
||||
}
|
||||
final settings = context.getSettings();
|
||||
final radius = settings.selectSensitivity / transform.size;
|
||||
final hits = await rayCast(globalPos, context.buildContext, radius);
|
||||
final hits = await rayCast(globalPos, context.getDocumentBloc(),
|
||||
context.getCameraTransform(), radius);
|
||||
if (hits.isEmpty) {
|
||||
if (!context.isCtrlPressed) {
|
||||
_selected.clear();
|
||||
@ -336,7 +337,8 @@ class HandHandler extends Handler<HandPainter> {
|
||||
return;
|
||||
}
|
||||
final position = context.getCameraTransform().localToGlobal(localPosition);
|
||||
final hits = await rayCast(position, context.buildContext, 0.0);
|
||||
final hits = await rayCast(
|
||||
position, context.getDocumentBloc(), context.getCameraTransform(), 0.0);
|
||||
final hit = hits.firstOrNull;
|
||||
final rect = hit?.rect;
|
||||
if ((rect != null && !(getSelectionRect()?.contains(position) ?? false)) &&
|
||||
@ -470,7 +472,8 @@ class HandHandler extends Handler<HandPainter> {
|
||||
if (!context.isCtrlPressed) {
|
||||
_selected.clear();
|
||||
}
|
||||
final hits = await rayCastRect(freeSelection, context.buildContext);
|
||||
final hits = await rayCastRect(freeSelection, context.getDocumentBloc(),
|
||||
context.getCameraTransform());
|
||||
_selected.addAll(hits);
|
||||
context.refresh();
|
||||
}
|
||||
|
@ -267,21 +267,22 @@ class _RayCastParams {
|
||||
|
||||
Future<Set<Renderer<PadElement>>> rayCast(
|
||||
Offset globalPosition,
|
||||
BuildContext context,
|
||||
DocumentBloc bloc,
|
||||
CameraTransform transform,
|
||||
double radius,
|
||||
) async {
|
||||
return rayCastRect(
|
||||
Rect.fromCircle(center: globalPosition, radius: radius),
|
||||
context,
|
||||
bloc,
|
||||
transform,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Set<Renderer<PadElement>>> rayCastRect(
|
||||
Rect rect,
|
||||
BuildContext context,
|
||||
DocumentBloc bloc,
|
||||
CameraTransform transform,
|
||||
) async {
|
||||
final bloc = context.read<DocumentBloc>();
|
||||
final transform = context.read<TransformCubit>().state;
|
||||
final state = bloc.state;
|
||||
if (state is! DocumentLoadSuccess) return {};
|
||||
final renderers = state.cameraViewport.visibleElements;
|
||||
|
@ -9,8 +9,11 @@ class LayerHandler extends Handler<LayerPainter> {
|
||||
final transform = context.getCameraTransform();
|
||||
final state = context.getState();
|
||||
if (state == null) return;
|
||||
final hits = await rayCast(transform.localToGlobal(event.localPosition),
|
||||
context.buildContext, data.strokeWidth / transform.size);
|
||||
final hits = await rayCast(
|
||||
transform.localToGlobal(event.localPosition),
|
||||
context.getDocumentBloc(),
|
||||
context.getCameraTransform(),
|
||||
data.strokeWidth / transform.size);
|
||||
context.addDocumentEvent(ElementsLayerChanged(
|
||||
state.currentLayer, hits.map((e) => e.element).toList()));
|
||||
}
|
||||
|
@ -12,7 +12,8 @@ class PathEraserHandler extends Handler<PathEraserPainter> {
|
||||
_removeRunning = true;
|
||||
final hits = await rayCast(
|
||||
transform.localToGlobal(event.localPosition),
|
||||
context.buildContext,
|
||||
context.getDocumentBloc(),
|
||||
context.getCameraTransform(),
|
||||
data.strokeWidth / transform.size,
|
||||
);
|
||||
context
|
||||
|
@ -4,30 +4,30 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:material_leap/material_leap.dart';
|
||||
import 'package:phosphor_flutter/phosphor_flutter.dart';
|
||||
|
||||
extension ArangementVisualizer on Arangement {
|
||||
extension ArangementVisualizer on Arrangement {
|
||||
String getLocalizedName(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
switch (this) {
|
||||
case Arangement.back:
|
||||
case Arrangement.back:
|
||||
return loc.sendToBack;
|
||||
case Arangement.front:
|
||||
case Arrangement.front:
|
||||
return loc.bringToFront;
|
||||
case Arangement.backward:
|
||||
case Arrangement.backward:
|
||||
return loc.sendBackward;
|
||||
case Arangement.forward:
|
||||
case Arrangement.forward:
|
||||
return loc.bringForward;
|
||||
}
|
||||
}
|
||||
|
||||
IconGetter get icon {
|
||||
switch (this) {
|
||||
case Arangement.back:
|
||||
case Arrangement.back:
|
||||
return PhosphorIcons.arrowDown;
|
||||
case Arangement.front:
|
||||
case Arrangement.front:
|
||||
return PhosphorIcons.arrowUp;
|
||||
case Arangement.backward:
|
||||
case Arrangement.backward:
|
||||
return PhosphorIcons.arrowDownLeft;
|
||||
case Arangement.forward:
|
||||
case Arrangement.forward:
|
||||
return PhosphorIcons.arrowUpRight;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user