Skip to content
Merged
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
7 changes: 5 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,12 @@ three pass.
(components stored in `<project>/.flame_editor/components/<name>.json`) —
see "Existing modules" in [docs/architecture.md](docs/architecture.md).
- Component types: `rectangle` (position — edited only on children —,
size, and paint with color and fill style) and `position` (a plain Flame
size, and paint with color and fill style), `position` (a plain Flame
`PositionComponent`: position — children only — and size; draws nothing
itself, useful as a container). Components can have children
itself, useful as a container) and `circle` (a Flame `CircleComponent`:
position — children only —, radius, and paint; the radius is its own
`ComponentRadius` property, since Flame circles take a radius, not a
size). Components can have children
(see "Component tree" in [docs/architecture.md](docs/architecture.md)). Properties are shared between types through capability interfaces —
see "Component types & shared properties" in
[docs/architecture.md](docs/architecture.md) before adding a type or
Expand Down
14 changes: 8 additions & 6 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,13 @@ component types only declare which ones they have:

| Piece | Where | Example |
| --- | --- | --- |
| Value object (Equatable, JSON, defaults) | `component_repository/lib/src/models/` | `ComponentPosition`, `ComponentSize`, `ComponentPaint` |
| Capability interface | `component_repository/lib/src/models/component_capabilities.dart` | `PositionedComponent`, `SizedComponent`, `PaintedComponent` |
| Component type | `ComponentType` enum + a subclass in `component.dart`'s library (`part` file) | `RectangleComponent implements PositionedComponent, SizedComponent, PaintedComponent`, `PositionComponent implements PositionedComponent, SizedComponent` |
| Value object (Equatable, JSON, defaults) | `component_repository/lib/src/models/` | `ComponentPosition`, `ComponentSize`, `ComponentRadius`, `ComponentPaint` |
| Capability interface | `component_repository/lib/src/models/component_capabilities.dart` | `PositionedComponent`, `SizedComponent`, `RadiusComponent`, `PaintedComponent` |
| Component type | `ComponentType` enum + a subclass in `component.dart`'s library (`part` file) | `RectangleComponent implements PositionedComponent, SizedComponent, PaintedComponent`, `PositionComponent implements PositionedComponent, SizedComponent`, `CircleComponent implements PositionedComponent, RadiusComponent, PaintedComponent` |
| Bloc event | `lib/component_editor/bloc/` | `ComponentEditorSizeChanged` (edits the selected node if it is a `SizedComponent`) |
| Editor widget (`value` + `onChanged`, bloc-agnostic) | `lib/component_editor/widgets/property_editors/` | `SizeEditor`, `PaintEditor`, built from `NumberField`, `ColorField`, `EnumField` |
| Editor widget (`value` + `onChanged`, bloc-agnostic) | `lib/component_editor/widgets/property_editors/` | `SizeEditor`, `RadiusEditor`, `PaintEditor`, built from `NumberField`, `ColorField`, `EnumField` |
| Panel wiring | `lib/component_editor/widgets/properties_panel.dart` | `if (component case SizedComponent c) SizeEditor(...)` |
| Flame mapping | `lib/component_editor/widgets/preview/flame_mapping.dart` | `applyPosition`, `applySize`, `applyPaint`, `buildFlameComponent` |
| Flame mapping | `lib/component_editor/widgets/preview/flame_mapping.dart` | `applyPosition`, `applySize`, `applyRadius`, `applyPaint`, `buildFlameComponent` |
| Code generation | `component_repository/lib/src/code_generation/` | `ComponentCodeGenerator._flameClass` / `_arguments`, `flameMemberNames` in `dart_names.dart` |

**Adding a component type:** add a `ComponentType` value, a `Component`
Expand Down Expand Up @@ -198,7 +198,9 @@ only: the generated code uses a plain `PositionComponent`.

The preview highlights the selected node with a magenta border while
`isSelectionHighlighted` is on (the toolbar toggle; on by default, not
persisted). Only nodes with a size (`SizedComponent`) are highlighted. The
persisted). Only nodes with a size (`SizedComponent`) or a radius
(`RadiusComponent`, whose Flame size is the circle's bounding box) are
highlighted. The
border is drawn by `SelectionHighlight`, a world-level overlay that targets
the Flame component found with `flameNodeAt`, rather than a child of it:
that keeps it above overlapping siblings and keeps the Flame children
Expand Down
13 changes: 13 additions & 0 deletions lib/component_editor/bloc/component_editor_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class ComponentEditorBloc
on<ComponentEditorNameChanged>(_onNameChanged);
on<ComponentEditorPositionChanged>(_onPositionChanged);
on<ComponentEditorSizeChanged>(_onSizeChanged);
on<ComponentEditorRadiusChanged>(_onRadiusChanged);
on<ComponentEditorPaintChanged>(_onPaintChanged);
on<ComponentEditorSelectionHighlightToggled>(_onSelectionHighlightToggled);
on<ComponentEditorSaveRequested>(_onSaveRequested);
Expand Down Expand Up @@ -192,6 +193,18 @@ class ComponentEditorBloc
});
}

void _onRadiusChanged(
ComponentEditorRadiusChanged event,
Emitter<ComponentEditorState> emit,
) {
_editSelected(emit, (node) {
if (node case final RadiusComponent node) {
return node.copyWithRadius(event.radius);
}
return node;
});
}

void _onPaintChanged(
ComponentEditorPaintChanged event,
Emitter<ComponentEditorState> emit,
Expand Down
12 changes: 12 additions & 0 deletions lib/component_editor/bloc/component_editor_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,18 @@ final class ComponentEditorSizeChanged extends ComponentEditorEvent {
List<Object> get props => [size];
}

/// The user edited the radius of the selected [RadiusComponent].
final class ComponentEditorRadiusChanged extends ComponentEditorEvent {
/// Creates a [ComponentEditorRadiusChanged] event.
const new(this.radius);

/// The new radius.
final ComponentRadius radius;

@override
List<Object> get props => [radius];
}

/// The user edited the paint of the selected [PaintedComponent].
final class ComponentEditorPaintChanged extends ComponentEditorEvent {
/// Creates a [ComponentEditorPaintChanged] event.
Expand Down
2 changes: 1 addition & 1 deletion lib/component_editor/bloc/component_editor_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ final class ComponentEditorState extends Equatable {
final bool isCodeOutdated;

/// Whether the preview highlights the selected node, when it has a size
/// (see [SizedComponent]).
/// or a radius (see [SizedComponent] and [RadiusComponent]).
final bool isSelectionHighlighted;

/// Whether the component can be saved: it has changes or its code is
Expand Down
1 change: 1 addition & 0 deletions lib/component_editor/widgets/component_tree_panel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ class ComponentTreePanel extends StatelessWidget {
return switch (type) {
ComponentType.rectangle => Icons.crop_square,
ComponentType.position => Icons.crop_free,
ComponentType.circle => Icons.circle_outlined,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ class ComponentPreviewGame extends FlameGame {
/// Renders [component] instead of the current one, highlighting the node
/// at [highlightedPath] (see [ComponentTree]), if any.
///
/// Only nodes with a size ([SizedComponent]) are highlighted.
/// Only nodes with a size or a radius ([SizedComponent],
/// [RadiusComponent]) are highlighted.
///
/// The existing Flame components are updated in place, unless components
/// were added, removed or changed type (see [haveSameShape]).
Expand Down Expand Up @@ -68,8 +69,12 @@ class ComponentPreviewGame extends FlameGame {
highlight.target =
path != null &&
_component.hasNodeAt(path) &&
_component.nodeAt(path) is SizedComponent
_hasBounds(_component.nodeAt(path))
? flameNodeAt(_preview, path)
: null;
}

static bool _hasBounds(Component node) {
return node is SizedComponent || node is RadiusComponent;
}
}
10 changes: 10 additions & 0 deletions lib/component_editor/widgets/preview/flame_mapping.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ flame.PositionComponent _placeRoot(flame.PositionComponent root) {
flame.PositionComponent _build(Component component) {
final target = switch (component) {
RectangleComponent() => flame.RectangleComponent(),
CircleComponent() => flame.CircleComponent(),
// Outlined, since a plain PositionComponent would be invisible.
PositionComponent() => PreviewPositionComponent(),
};
Expand Down Expand Up @@ -95,6 +96,10 @@ T applyProperties<T extends flame.PositionComponent>(
if (component case final SizedComponent sized) {
applySize(target, sized.size);
}
if (component case final RadiusComponent round
when target is flame.CircleComponent) {
applyRadius(target as flame.CircleComponent, round.radius);
}
if (component case final PaintedComponent painted
when target is flame.HasPaint) {
applyPaint(target as flame.HasPaint, painted.paint);
Expand All @@ -112,6 +117,11 @@ void applySize(flame.PositionComponent target, ComponentSize size) {
target.size.setValues(size.width, size.height);
}

/// Applies [radius] to [target], which also sets its size.
void applyRadius(flame.CircleComponent target, ComponentRadius radius) {
target.radius = radius.radius;
}

/// Applies [paint] to [target].
void applyPaint(flame.HasPaint target, ComponentPaint paint) {
target.paint
Expand Down
6 changes: 6 additions & 0 deletions lib/component_editor/widgets/properties_panel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ class PropertiesPanel extends StatelessWidget {
value: sized.size,
onChanged: (size) => bloc.add(ComponentEditorSizeChanged(size)),
),
if (component case final RadiusComponent round)
RadiusEditor(
value: round.radius,
onChanged: (radius) =>
bloc.add(ComponentEditorRadiusChanged(radius)),
),
if (component case final PaintedComponent painted)
PaintEditor(
value: painted.paint,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export 'number_field.dart';
export 'paint_editor.dart';
export 'position_editor.dart';
export 'property_section.dart';
export 'radius_editor.dart';
export 'size_editor.dart';
32 changes: 32 additions & 0 deletions lib/component_editor/widgets/property_editors/radius_editor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import 'package:component_repository/component_repository.dart';
import 'package:flame_component_editor/component_editor/widgets/property_editors/number_field.dart';
import 'package:flame_component_editor/component_editor/widgets/property_editors/property_section.dart';
import 'package:flutter/material.dart';

/// {@template radius_editor}
/// Edits a [ComponentRadius]. Used by every [RadiusComponent].
/// {@endtemplate}
class RadiusEditor extends StatelessWidget {
/// {@macro radius_editor}
const new({required this.value, required this.onChanged, super.key});

/// The current radius.
final ComponentRadius value;

/// Called with the edited radius.
final ValueChanged<ComponentRadius> onChanged;

@override
Widget build(BuildContext context) {
return PropertySection(
title: 'Radius',
children: [
NumberField(
label: 'Radius',
value: value.radius,
onChanged: (radius) => onChanged(value.copyWith(radius: radius)),
),
],
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ class ComponentCodeGenerator {
return switch (component) {
RectangleComponent() => 'RectangleComponent',
PositionComponent() => 'PositionComponent',
CircleComponent() => 'CircleComponent',
};
}

Expand All @@ -142,6 +143,8 @@ class ComponentCodeGenerator {
'position: ${_vector(c.position.x, c.position.y)}',
if (component case final SizedComponent c)
'size: ${_vector(c.size.width, c.size.height)}',
if (component case final RadiusComponent c)
'radius: ${_number(c.radius.radius)}',
if (component case final PaintedComponent c) 'paint: ${_paint(c.paint)}',
];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Set<String> flameMemberNames(ComponentType type) {
return switch (type) {
ComponentType.rectangle => _rectangleComponentMembers,
ComponentType.position => _positionComponentMembers,
ComponentType.circle => _circleComponentMembers,
};
}

Expand Down Expand Up @@ -231,3 +232,35 @@ const Set<String> _rectangleComponentMembers = {
'tint',
'vertices',
};

/// Public members of Flame's `CircleComponent` (Flame 2.0.0-dev.1),
/// including the inherited ones: the `PositionComponent` ones, plus those
/// of `HasPaint` and `ShapeComponent`.
const Set<String> _circleComponentMembers = {
..._positionComponentMembers,
'deletePaint',
'getAlpha',
'getOpacity',
'getPaint',
'hasPaintLayers',
'hue',
'isSolid',
'lineSegmentIntersections',
'makeOpaque',
'makeTransparent',
'onChanged',
'opacity',
'opacityProviderOf',
'opacityProviderOfList',
'paint',
'paintLayers',
'paintLayersInternal',
'radius',
'renderShape',
'scaledRadius',
'setAlpha',
'setColor',
'setOpacity',
'setPaint',
'tint',
};
90 changes: 90 additions & 0 deletions packages/component_repository/lib/src/models/circle_component.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
part of 'component.dart';

/// {@template circle_component}
/// A circle with a position, a radius and a paint.
/// {@endtemplate}
final class CircleComponent extends Component
implements PositionedComponent, RadiusComponent, PaintedComponent {
/// {@macro circle_component}
const new({
required super.name,
this.position = const ComponentPosition(),
this.radius = const ComponentRadius(),
this.paint = const ComponentPaint(),
super.children,
});

/// Reads the properties of a [CircleComponent] from its JSON
/// representation. Use [Component.fromJson], which also reads the
/// children.
factory _fromJson(Map<String, dynamic> json) {
return CircleComponent(
name: json['name'] as String,
position: ComponentPosition.fromJson(json['position']),
radius: ComponentRadius.fromJson(json['radius']),
paint: ComponentPaint.fromJson(json['paint']),
);
}

@override
ComponentType get type => ComponentType.circle;

@override
final ComponentPosition position;

@override
final ComponentRadius radius;

@override
final ComponentPaint paint;

@override
CircleComponent copyWithPosition(ComponentPosition position) {
return _copyWith(position: position);
}

@override
CircleComponent copyWithRadius(ComponentRadius radius) {
return _copyWith(radius: radius);
}

@override
CircleComponent copyWithPaint(ComponentPaint paint) {
return _copyWith(paint: paint);
}

@override
CircleComponent copyWithName(String name) => _copyWith(name: name);

@override
CircleComponent copyWithChildren(List<Component> children) {
return _copyWith(children: children);
}

CircleComponent _copyWith({
String? name,
ComponentPosition? position,
ComponentRadius? radius,
ComponentPaint? paint,
List<Component>? children,
}) {
return CircleComponent(
name: name ?? this.name,
position: position ?? this.position,
radius: radius ?? this.radius,
paint: paint ?? this.paint,
children: children ?? this.children,
);
}

@override
Map<String, dynamic> toJson() => {
...super.toJson(),
'position': position.toJson(),
'radius': radius.toJson(),
'paint': paint.toJson(),
};

@override
List<Object> get props => [...super.props, position, radius, paint];
}
3 changes: 3 additions & 0 deletions packages/component_repository/lib/src/models/component.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:component_repository/component_repository.dart';
import 'package:equatable/equatable.dart';

part 'circle_component.dart';
part 'position_component.dart';
part 'rectangle_component.dart';

Expand All @@ -23,6 +24,7 @@ sealed class Component extends Equatable {
return switch (type) {
ComponentType.rectangle => RectangleComponent(name: name),
ComponentType.position => PositionComponent(name: name),
ComponentType.circle => CircleComponent(name: name),
};
}

Expand All @@ -36,6 +38,7 @@ sealed class Component extends Equatable {
final component = switch (type) {
ComponentType.rectangle => RectangleComponent._fromJson(json),
ComponentType.position => PositionComponent._fromJson(json),
ComponentType.circle => CircleComponent._fromJson(json),
};
return component.copyWithChildren(_childrenFromJson(json['children']));
}
Expand Down
Loading
Loading