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
2 changes: 2 additions & 0 deletions .github/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
"cubit",
"cubits",
"flame",
"hitbox",
"hitboxes",
"lerp",
"LTRB",
"LTWH",
Expand Down
11 changes: 9 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ three pass.
(a top bar below the app bar for project-wide actions — the palette
button —, component tree on the left to add/remove/select/rename nested children,
live Flame preview in the center, with a toolbar above it whose toggle
highlights the selected node with a magenta border and whose zoom
highlights the selected node with a magenta border, another toggle
shows/hides the hitbox outlines, and whose zoom
controls (also Cmd/Ctrl + `=`/`-`/`0`) zoom the preview — the zoom is an
app-wide preference per component, in `editor_preferences_repository`,
never in the component's JSON —, properties of the selected node on the
Expand All @@ -87,7 +88,13 @@ three pass.
itself, useful as a container) and `circle` (a Flame `CircleComponent`:
position, anchor and angle — 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
size) and the hitboxes `rectangleHitbox`, `circleHitbox` and
`polygonHitbox` (Flame's `RectangleHitbox`, `CircleHitbox` and
`PolygonHitbox`: their shape's geometry plus a `ComponentCollision` —
collision type and solid —, no paint; children only, never roots, and
with no children of their own; outlined in orange dashes in the preview,
shown/hidden by a toolbar toggle — see "Hitboxes" in
[docs/architecture.md](docs/architecture.md)). 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
50 changes: 42 additions & 8 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,10 @@ dependencies:
| Data | `packages/file_system_client` | Native directory picker (`file_selector`), directory/file existence, list/read/write/delete files |
| Repository | `packages/editor_preferences_repository` | App-wide editor preferences (`shared_preferences`, through `key_value_storage_client`), not stored in the project: the preview zoom of each component, keyed by project path + component name (`component_zoom:<projectPath>:<componentName>`), clamped to `minZoom`–`maxZoom`; the `ZoomLevels` the editor canvases step through |
| Repository | `packages/project_repository` | `Project` and `ProjectSettings` models; pick/open projects; last 10 recent projects; read/write project settings (`<project>/.flame_editor/settings.json`) and pick a project-relative output folder |
| Repository | `packages/component_repository` | Sealed `Component` model (one subclass per `ComponentType`), shared property models (`ComponentPosition`, `ComponentSize`, `ComponentPaint`, `ComponentVertices`…), the `PolygonShape` presets new polygons start with, capability interfaces and the `ComponentTree` extension for nested children (including child name validation); list/create/save/delete a project's components (one JSON file each in `<project>/.flame_editor/components/`); `ComponentCodeGenerator` and writing/deleting the generated Dart code (see "Code generation"); the project's `Palette` (`<project>/.flame_editor/palette.json`) and its `PaletteCodeGenerator` (see "Palette") |
| Repository | `packages/component_repository` | Sealed `Component` model (one subclass per `ComponentType`), shared property models (`ComponentPosition`, `ComponentSize`, `ComponentPaint`, `ComponentVertices`…), the `PolygonShape` presets new polygons start with, the hitbox types and their `ComponentCollision`, capability interfaces and the `ComponentTree` extension for nested children (including child name validation); list/create/save/delete a project's components (one JSON file each in `<project>/.flame_editor/components/`); `ComponentCodeGenerator` and writing/deleting the generated Dart code (see "Code generation"); the project's `Palette` (`<project>/.flame_editor/palette.json`) and its `PaletteCodeGenerator` (see "Palette") |
| Bloc + view | `lib/home/` | Landing screen: "Open project" button and recent projects list |
| Bloc + view | `lib/project/` | Project dashboard: lists components, add (name + type dialog → editor), edit, delete (with confirmation; also deletes the generated code), settings icon (opens the settings dialog). Pushed from home with `ProjectPage.route` |
| Bloc + view | `lib/component_editor/` | Component editor, `ComponentEditorPage.route(project, component)`: a top bar (`EditorTopBar`, below the app bar, above the three panels) for project-wide actions (the palette button), component tree (left, add/remove children, root selected by default), Flame preview (center, magenta border around the selected node; a toolbar above it toggles that highlight and has the zoom controls), properties panel for the selected node (right; children also get a Name field), save button + Cmd/Ctrl+S (also generates the Dart code), unsaved-changes marker in the title, `PopScope` prompt (cancel / discard / save) when leaving with unsaved changes |
| Bloc + view | `lib/component_editor/` | Component editor, `ComponentEditorPage.route(project, component)`: a top bar (`EditorTopBar`, below the app bar, above the three panels) for project-wide actions (the palette button), component tree (left, add/remove children — not under hitboxes —, root selected by default), Flame preview (center, magenta border around the selected node; a toolbar above it toggles that highlight and the hitbox outlines, and has the zoom controls; it scrolls horizontally when the preview is narrow), properties panel for the selected node (right; children also get a Name field), save button + Cmd/Ctrl+S (also generates the Dart code), unsaved-changes marker in the title, `PopScope` prompt (cancel / discard / save) when leaving with unsaved changes |
| Bloc + view | `lib/vertices_editor/` | Vertices editor dialog, `VerticesEditorDialog.show(context, vertices:, paint:)`: a `CustomPaint` canvas (`VerticesCanvas`, `VerticesPainter`) where the user drags vertices, adds one from the "+" handle in the middle of an edge, and deletes the selected one (Delete/Backspace, context menu or button; at least 3 are kept). A side bar (`VerticesList`) has X/Y fields per vertex to type exact coordinates, and a delete button per row; selecting a row or focusing its fields selects the vertex. `VerticesEditorCubit` edits a working copy; Apply returns it normalized, Cancel/Esc returns `null`. `VerticesViewTransform` maps local coordinates to the canvas (fitted once when the dialog opens) and does the hit testing. The canvas zooms (`ZoomControls` next to the title, Cmd/Ctrl + `=`/`-`/`0`) relative to that fit, around its center (`VerticesViewTransform.zoomed`); the zoom is held by the cubit and not remembered |
| Bloc + view | `lib/palette/` | Palette dialog, `PaletteDialog.show(context, project:, component:)` (opened from the editor's top bar; returns the saved `Palette` or `null`): add, rename, change and remove named colors; colors used by a component (including the unsaved edits of the open one) can't be renamed or removed. `PaletteBloc` saves the palette and generates its code |
| Bloc + view | `lib/project_settings/` | Project settings dialog, `ProjectSettingsDialog.show(context, project)`: output path for generated Dart code (default `lib/components`, text field + Browse) |
Expand All @@ -154,13 +154,13 @@ component types only declare which ones they have:

| Piece | Where | Example |
| --- | --- | --- |
| Value object (Equatable, JSON, defaults) | `component_repository/lib/src/models/` | `ComponentPosition`, `ComponentAnchor`, `ComponentAngle`, `ComponentSize`, `ComponentRadius`, `ComponentVertices`, `ComponentPaint` |
| Capability interface | `component_repository/lib/src/models/component_capabilities.dart` | `PositionedComponent`, `AnchoredComponent`, `RotatedComponent`, `SizedComponent`, `RadiusComponent`, `VerticesComponent`, `PaintedComponent` |
| Component type | `ComponentType` enum + a subclass in `component.dart`'s library (`part` file) | `RectangleComponent implements PositionedComponent, AnchoredComponent, RotatedComponent, SizedComponent, PaintedComponent`, `PositionComponent implements PositionedComponent, AnchoredComponent, RotatedComponent, SizedComponent`, `CircleComponent implements PositionedComponent, AnchoredComponent, RotatedComponent, RadiusComponent, PaintedComponent`, `PolygonComponent implements PositionedComponent, AnchoredComponent, RotatedComponent, VerticesComponent, PaintedComponent` |
| Value object (Equatable, JSON, defaults) | `component_repository/lib/src/models/` | `ComponentPosition`, `ComponentAnchor`, `ComponentAngle`, `ComponentSize`, `ComponentRadius`, `ComponentVertices`, `ComponentPaint`, `ComponentCollision` |
| Capability interface | `component_repository/lib/src/models/component_capabilities.dart` | `PositionedComponent`, `AnchoredComponent`, `RotatedComponent`, `SizedComponent`, `RadiusComponent`, `VerticesComponent`, `PaintedComponent`, `HitboxComponent` |
| Component type | `ComponentType` enum + a subclass in `component.dart`'s library (`part` file) | `RectangleComponent implements PositionedComponent, AnchoredComponent, RotatedComponent, SizedComponent, PaintedComponent`, `PositionComponent implements PositionedComponent, AnchoredComponent, RotatedComponent, SizedComponent`, `CircleComponent implements PositionedComponent, AnchoredComponent, RotatedComponent, RadiusComponent, PaintedComponent`, `PolygonComponent implements PositionedComponent, AnchoredComponent, RotatedComponent, VerticesComponent, PaintedComponent`, and the hitboxes, which swap the paint for a `HitboxComponent`: `RectangleHitboxComponent`, `CircleHitboxComponent`, `PolygonHitboxComponent` |
| 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/` | `PositionEditor`, `AnchorEditor`, `AngleEditor`, `SizeEditor`, `RadiusEditor`, `VerticesEditor` (a button that opens the vertices editor dialog), `PaintEditor`, built from `NumberField`, `ColorField`, `EnumField` |
| Editor widget (`value` + `onChanged`, bloc-agnostic) | `lib/component_editor/widgets/property_editors/` | `PositionEditor`, `AnchorEditor`, `AngleEditor`, `SizeEditor`, `RadiusEditor`, `VerticesEditor` (a button that opens the vertices editor dialog), `PaintEditor`, `CollisionEditor`, 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`, `applyAnchor`, `applyAngle`, `applySize`, `applyRadius`, `applyPaint`, `buildFlameComponent` |
| Flame mapping | `lib/component_editor/widgets/preview/flame_mapping.dart` | `applyPosition`, `applyAnchor`, `applyAngle`, `applySize`, `applyRadius`, `applyPaint`, `applyCollision`, `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 @@ -199,6 +199,38 @@ rebuilds it. Flame also changes the list it is given, so the preview and
the generated code always pass a new, non-const list, as the first
(positional) constructor argument.

### Hitboxes

`RectangleHitboxComponent`, `CircleHitboxComponent` and
`PolygonHitboxComponent` are Flame's `RectangleHitbox`, `CircleHitbox` and
`PolygonHitbox`: the geometry of their shape (size, radius or vertices, so
the shared editors, highlight and polygon rules apply) without a paint,
plus a `ComponentCollision` (`HitboxComponent`): the collision type
(`HitboxCollisionType`, mirroring Flame's `CollisionType`) and `isSolid`.
The generated code imports `package:flame/collisions.dart` when the tree has
a hitbox, and omits `collisionType`/`isSolid` at Flame's defaults (active,
not solid).

- **Children only, and leaves.** Flame throws when a hitbox has no
`PositionComponent` ancestor, so `createComponent` rejects hitbox roots
(`ArgumentError`) and the add component dialog doesn't offer them;
`addChildAt` throws under a hitbox, the editor bloc ignores
`ComponentEditorChildAdded` while one is selected and the tree's add
button is disabled.
- **Preview.** Hitboxes draw nothing, so the preview builds
`PreviewRectangleHitbox`, `PreviewCircleHitbox` and `PreviewPolygonHitbox`
(`preview_hitboxes.dart`, sharing the `PreviewHitbox` mixin): the real
Flame hitboxes with an **orange dashed outline**. The preview game has no
collision detection, so they never collide. The toolbar's "Show
hitboxes" toggle (`ComponentEditorState.areHitboxesVisible`, on by
default, not persisted) hides them through Flame's `HasVisibility`
(`ComponentPreviewGame.areHitboxesVisible`). The vertices editor draws
polygon hitboxes with the same orange stroke.
- **Fill-parent gotcha.** Flame's `RectangleHitbox`/`CircleHitbox` fill
their parent when built without a size/radius and without a position.
The preview hitboxes are built with a zero size/radius, and the
generated children always pass a position and a size/radius.

### Component tree

Like in Flame, every `Component` has `children` (stored nested in the same
Expand All @@ -223,7 +255,9 @@ updates the Flame components in place while the tree shape is unchanged
Position components draw nothing in Flame, so the preview builds them as
`PreviewPositionComponent`, a `PositionComponent` with a faint dashed
outline, to keep them visible while unselected. The outline is preview
only: the generated code uses a plain `PositionComponent`.
only: the generated code uses a plain `PositionComponent`. The dashes are
drawn by the shared helpers in `dashed_outline.dart`, also used by the
hitboxes.

The preview highlights the selected node with a magenta border while
`isSelectionHighlighted` is on (the toolbar toggle; on by default, not
Expand Down
23 changes: 23 additions & 0 deletions lib/component_editor/bloc/component_editor_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ class ComponentEditorBloc
on<ComponentEditorRadiusChanged>(_onRadiusChanged);
on<ComponentEditorVerticesChanged>(_onVerticesChanged);
on<ComponentEditorPaintChanged>(_onPaintChanged);
on<ComponentEditorCollisionChanged>(_onCollisionChanged);
on<ComponentEditorPaletteChanged>(_onPaletteChanged);
on<ComponentEditorSelectionHighlightToggled>(_onSelectionHighlightToggled);
on<ComponentEditorHitboxesVisibilityToggled>(_onHitboxesVisibilityToggled);
on<ComponentEditorZoomedIn>(_onZoomedIn);
on<ComponentEditorZoomedOut>(_onZoomedOut);
on<ComponentEditorZoomReset>(_onZoomReset);
Expand Down Expand Up @@ -158,6 +160,8 @@ class ComponentEditorBloc
ComponentEditorChildAdded event,
Emitter<ComponentEditorState> emit,
) {
// Hitboxes can't have children.
if (state.selectedNode is HitboxComponent) return;
final component = state.component;
final parentPath = state.selectedPath;
final child = Component.create(
Expand Down Expand Up @@ -294,6 +298,18 @@ class ComponentEditorBloc
});
}

void _onCollisionChanged(
ComponentEditorCollisionChanged event,
Emitter<ComponentEditorState> emit,
) {
_editSelected(emit, (node) {
if (node case final HitboxComponent node) {
return node.copyWithCollision(event.collision);
}
return node;
});
}

void _onPaletteChanged(
ComponentEditorPaletteChanged event,
Emitter<ComponentEditorState> emit,
Expand All @@ -319,6 +335,13 @@ class ComponentEditorBloc
emit(state.copyWith(isSelectionHighlighted: !state.isSelectionHighlighted));
}

void _onHitboxesVisibilityToggled(
ComponentEditorHitboxesVisibilityToggled event,
Emitter<ComponentEditorState> emit,
) {
emit(state.copyWith(areHitboxesVisible: !state.areHitboxesVisible));
}

Future<void> _onZoomedIn(
ComponentEditorZoomedIn event,
Emitter<ComponentEditorState> emit,
Expand Down
19 changes: 19 additions & 0 deletions lib/component_editor/bloc/component_editor_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -169,13 +169,32 @@ final class ComponentEditorPaletteChanged extends ComponentEditorEvent {
List<Object> get props => [palette];
}

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

/// The new collision.
final ComponentCollision collision;

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

/// The user toggled the highlight of the selected node in the preview.
final class ComponentEditorSelectionHighlightToggled
extends ComponentEditorEvent {
/// Creates a [ComponentEditorSelectionHighlightToggled] event.
const new();
}

/// The user toggled the outlines of the hitboxes in the preview.
final class ComponentEditorHitboxesVisibilityToggled
extends ComponentEditorEvent {
/// Creates a [ComponentEditorHitboxesVisibilityToggled] event.
const new();
}

/// The user zoomed in the preview, to the next of
/// [ZoomLevels].
final class ComponentEditorZoomedIn extends ComponentEditorEvent {
Expand Down
8 changes: 8 additions & 0 deletions lib/component_editor/bloc/component_editor_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ final class ComponentEditorState extends Equatable {
this.takenClassNames = const {},
this.isCodeOutdated = false,
this.isSelectionHighlighted = true,
this.areHitboxesVisible = true,
this.palette = const Palette(),
this.zoom = EditorPreferencesRepository.defaultZoom,
});
Expand Down Expand Up @@ -81,6 +82,10 @@ final class ComponentEditorState extends Equatable {
/// or a radius (see [SizedComponent] and [RadiusComponent]).
final bool isSelectionHighlighted;

/// Whether the preview outlines the hitboxes ([HitboxComponent]), which
/// draw nothing themselves.
final bool areHitboxesVisible;

/// The palette of the project, whose colors paints can use (see
/// [ComponentPaint.paletteColor]).
final Palette palette;
Expand Down Expand Up @@ -140,6 +145,7 @@ final class ComponentEditorState extends Equatable {
Map<String, Component>? takenClassNames,
bool? isCodeOutdated,
bool? isSelectionHighlighted,
bool? areHitboxesVisible,
Palette? palette,
double? zoom,
}) {
Expand All @@ -152,6 +158,7 @@ final class ComponentEditorState extends Equatable {
isCodeOutdated: isCodeOutdated ?? this.isCodeOutdated,
isSelectionHighlighted:
isSelectionHighlighted ?? this.isSelectionHighlighted,
areHitboxesVisible: areHitboxesVisible ?? this.areHitboxesVisible,
palette: palette ?? this.palette,
zoom: zoom ?? this.zoom,
);
Expand All @@ -166,6 +173,7 @@ final class ComponentEditorState extends Equatable {
takenClassNames,
isCodeOutdated,
isSelectionHighlighted,
areHitboxesVisible,
palette,
zoom,
];
Expand Down
Loading
Loading