Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/lib/cubits/current_index.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -211,6 +212,10 @@ class CurrentIndexCubit extends Cubit<CurrentIndex> {
emit(state.copyWith(sessionPenOnlyInput: value));
}

void toggleWireframeMode() {
emit(state.copyWith(wireframeMode: !state.wireframeMode));
}

Future<void> _updateOnVisible(
CameraViewport newViewport,
DocumentLoaded blocState,
Expand Down
23 changes: 13 additions & 10 deletions app/lib/cubits/current_index.freezed.dart

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions app/lib/handlers/grid.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class GridRenderer extends Renderer<GridTool> {
CameraTransform transform, [
ColorScheme? colorScheme,
bool foreground = false,
bool wireframeMode = false,
]) {
if (element.xSize > 0) {
double x = -element.xSize + element.xOffset % element.xSize;
Expand Down
1 change: 1 addition & 0 deletions app/lib/handlers/polygon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class PolygonSelectionRenderer extends Renderer<PolygonElement> {
CameraTransform transform, [
ColorScheme? colorScheme,
bool foreground = false,
bool wireframeMode = false,
]) {
if (element.points.isEmpty) return;
final strokeWidth = element.property.strokeWidth;
Expand Down
1 change: 1 addition & 0 deletions app/lib/handlers/ruler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ class RulerRenderer extends Renderer<RulerTool> {
CameraTransform transform, [
ColorScheme? colorScheme,
bool foreground = false,
bool wireframeMode = false,
]) {
canvas.save();
canvas.translate(transform.position.dx, transform.position.dy);
Expand Down
1 change: 1 addition & 0 deletions app/lib/handlers/spacer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
205 changes: 205 additions & 0 deletions app/lib/helpers/boolean_ops.dart
Original file line number Diff line number Diff line change
@@ -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<PolygonElement> performPathOperation(
List<PadElement> 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 = <PolygonElement>[];
final metrics = currentPath.computeMetrics();

for (final metric in metrics) {
final points = <PolygonPoint>[];
// 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<PolygonPoint> 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<PolygonPoint> 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);
}
}
18 changes: 17 additions & 1 deletion app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
1 change: 1 addition & 0 deletions app/lib/renderers/backgrounds/image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class ImageBackgroundRenderer extends Renderer<ImageBackground> {
CameraTransform transform, [
ColorScheme? colorScheme,
bool foreground = false,
bool wireframeMode = false,
]) {
if (image == null) return;
final sizeX = element.width * element.scaleX * transform.size;
Expand Down
1 change: 1 addition & 0 deletions app/lib/renderers/backgrounds/svg.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class SvgBackgroundRenderer extends Renderer<SvgBackground> {
CameraTransform transform, [
ColorScheme? colorScheme,
bool foreground = false,
bool wireframeMode = false,
]) {
if (_pictureInfo == null) return;
final sizeX = element.width * element.scaleX * transform.size;
Expand Down
1 change: 1 addition & 0 deletions app/lib/renderers/backgrounds/texture.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class TextureBackgroundRenderer extends Renderer<TextureBackground> {
CameraTransform transform, [
ColorScheme? colorScheme,
bool foreground = false,
bool wireframeMode = false,
]) => drawSurfaceTextureOnCanvas(
texture,
canvas,
Expand Down
1 change: 1 addition & 0 deletions app/lib/renderers/cursors/eraser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class EraserCursor extends Renderer<ToolCursorData<EraserInfo>> {
CameraTransform transform, [
ColorScheme? colorScheme,
bool foreground = false,
bool wireframeMode = false,
]) {
final radius = element.tool.strokeWidth / 2;
final position = transform.localToGlobal(element.position);
Expand Down
2 changes: 2 additions & 0 deletions app/lib/renderers/cursors/label.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class LabelCursor extends Renderer<LabelCursorData> {
CameraTransform transform, [
ColorScheme? colorScheme,
bool foreground = false,
bool wireframeMode = false,
]) {
const icon = PhosphorIconsLight.cursorText;
final property = switch (element.context) {
Expand Down Expand Up @@ -80,6 +81,7 @@ class LabelSelectionCursor extends Renderer<LabelContext> {
CameraTransform transform, [
ColorScheme? colorScheme,
bool foreground = false,
bool wireframeMode = false,
]) {
final color = colorScheme?.primary ?? Colors.blue;
// Paint vertical line
Expand Down
1 change: 1 addition & 0 deletions app/lib/renderers/cursors/user.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class UserCursor extends Renderer<NetworkingUser> {
CameraTransform transform, [
ColorScheme? colorScheme,
bool foreground = false,
bool wireframeMode = false,
]) {
final position = element.cursor?.toOffset();
if (position == null) {
Expand Down
9 changes: 9 additions & 0 deletions app/lib/renderers/elements/image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,16 @@ class ImageRenderer extends Renderer<ImageElement> {
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()
Expand Down
9 changes: 9 additions & 0 deletions app/lib/renderers/elements/pdf.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,16 @@ class PdfRenderer extends Renderer<PdfElement> {
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()
Expand Down
Loading
Loading