Implement arrangement

This commit is contained in:
CodeDoctorDE
2023-06-13 18:55:46 +02:00
parent 1a65a282c3
commit f39fdbbc80
16 changed files with 158 additions and 88 deletions

View File

@ -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.

View File

@ -0,0 +1,3 @@
export 'src/helpers/asset_helper.dart';
export 'src/helpers/point_helper.dart';
export 'src/helpers/search_helper.dart';

View 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';

View File

@ -1,3 +0,0 @@
export 'helpers/asset_helper.dart';
export 'helpers/point_helper.dart';
export 'helpers/search_helper.dart';

View File

@ -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';

View File

@ -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';

View File

@ -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) {

View File

@ -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 {

View File

@ -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

View File

@ -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)),

View File

@ -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>()

View File

@ -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();
}

View File

@ -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;

View File

@ -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()));
}

View File

@ -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

View File

@ -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;
}
}