strategy_forge 0.1.0
strategy_forge: ^0.1.0 copied to clipboard
An embeddable Flutter strategy editor with configurable layouts, drawing tools, markers, JSON persistence, and PNG export.
Strategy Forge #
Live example app · GitHub repository · Issue tracker
Strategy Forge is an embeddable Flutter strategy editor. It provides the canvas, editing behavior, responsive controls, document model, undo/redo history, JSON serialization, PNG generation, a complete tool catalog, and default tool artwork.
The package is intentionally domain-neutral:
- it does not contain a sport selector;
- it does not own field, court, rink, pool, or other strategy layouts;
- it does not provide a runtime layout upload interface;
- it does not persist documents or images;
- it does not require a particular state-management or backend package.
The embedding application supplies layouts, selects the enabled tools, optionally replaces icons or marker artwork, and decides where exported JSON and PNG bytes are stored.
Features #
- Host-defined asset, memory, or widget layouts with independent aspect ratios and thumbnails.
- 23 core editing tools covering selection, tactical paths, lines, shapes, text, and a generic marker.
- 34 bundled SVG markers from the original Pucks, Equipment, Nets, Players, Positions, and Numbers palettes.
- 29 generic Teams, Objects, Targets, and Training markers.
- 86 canonical catalog entries with stable IDs and compatibility aliases for former duplicates.
- Bundled default SVG icons plus
CustomIcon.ttfandMyFlutterApp.ttfglyphs. - Optional icon overrides for every tool.
- Configurable colors and stroke widths.
- Primary-color replacement for multicolor SVG markers while preserving outlines and details.
- A Default / Clear color action that restores original marker artwork.
- Multiline text creation and editing.
- Selection of strokes, text, and markers with move, scale, and rotate interactions.
- Separate scale handles and rotation handle, rotated selection frames, transform controls, and desktop cursors.
- Responsive toolbars for compact, short, tablet, and desktop constraints.
- Undo, redo, layout switching, and confirmed canvas clearing.
- Versioned JSON import/export with normalized coordinates.
- PNG export at a configurable pixel ratio.
- Host-controlled UI through
showControls: falseandStrategyEditorController.
Requirements and dependencies #
The current package version is 0.1.0. It requires Dart ^3.6.0 and Flutter 3.27 or later because it uses Flutter's wide-gamut color APIs.
| Dependency | Purpose |
|---|---|
| Flutter SDK | Widgets, gestures, painting, image capture, and platform rendering. |
flutter_svg ^2.3.0 |
Bundled and host-provided SVG tool artwork. |
flutter_lints ^6.0.0 |
Development lint rules. |
flutter_test |
Package unit and widget tests. |
The package itself does not depend on file-system, sharing, networking, database, or state-management libraries.
Installation #
After the first release is available on pub.dev, install it with:
flutter pub add strategy_forge
Or add the hosted dependency directly to the host app's pubspec.yaml:
dependencies:
strategy_forge: ^0.1.0
Then fetch dependencies:
flutter pub get
Import the public library:
import 'package:strategy_forge/strategy_forge.dart';
Before the first pub.dev release, developers can use the Git repository:
dependencies:
strategy_forge:
git:
url: https://github.com/Marlenfranto/Strategy-Forge.git
path: packages/strategy_forge
Quick start #
The controller belongs to the host screen. Create it once, render the editor in bounded space, and dispose it with the screen:
import 'package:flutter/material.dart';
import 'package:strategy_forge/strategy_forge.dart';
class StrategyScreen extends StatefulWidget {
const StrategyScreen({super.key});
@override
State<StrategyScreen> createState() => _StrategyScreenState();
}
class _StrategyScreenState extends State<StrategyScreen> {
late final StrategyEditorController controller;
@override
void initState() {
super.initState();
final config = StrategyEditorConfig(
id: 'training_view',
name: 'Training view',
layouts: [
StrategyLayout.asset(
id: 'full',
label: 'Full layout',
assetPath: 'assets/layouts/full.png',
aspectRatio: 2,
),
],
tools: StrategyToolCatalog.selectByIds(const [
'select',
'freehand',
'arrow',
'dashed_arrow',
'rectangle',
'text',
'team_home',
'team_away',
'equipment_cone',
]),
);
controller = StrategyEditorController(config: config);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: StrategyEditor(controller: controller),
),
);
}
}
Declare the host layout under flutter.assets in the host app:
flutter:
assets:
- assets/layouts/
StrategyEditor expands inside its parent. Place it in a Scaffold body, Expanded, or another widget that gives it finite width and height.
Responsibility split #
| Package | Embedding application |
|---|---|
| Canvas gestures and rendering | Layout images and layout selection policy |
| Built-in editing tools and default icons | Enabled tool set for each view |
| Document model and versioned JSON | Persistence, synchronization, and migrations |
| PNG byte generation | Download, upload, sharing, and file naming |
| Responsive built-in controls | App shell, navigation, authentication, and branding |
| Tool ID aliases | Product-specific presets and strategy categories |
Configuration reference #
StrategyEditorConfig #
| Property | Required | Description |
|---|---|---|
id |
Yes | Stable configuration identifier stored in every document. |
name |
Yes | Host-facing name for the configured editor. |
layouts |
Yes | Non-empty list of host-defined layouts. IDs must be unique. |
tools |
Yes | Non-empty list of enabled placement and editing tools. IDs must be unique. The first tool is initially active. |
markerRenderers |
No | Marker definitions used to render existing document markers even when their placement tools are hidden. Defaults to enabled marker tools. |
colors |
No | Non-empty palette. Defaults to black, red, orange, green, blue, and white. The first color is initially selected and is the drawing fallback after clearing the selection. |
strokeWidths |
No | Non-empty list of positive widths. Defaults to 2, 4, 6, and 8. The first width is initially selected. |
Construction rejects empty lists, duplicate layout or tool IDs, non-positive stroke widths, and non-marker entries in markerRenderers.
Layouts #
Every StrategyLayout has a stable id, visible label, positive aspectRatio, and widget builder. An optional thumbnailBuilder appears in the built-in layout menu.
Host asset
StrategyLayout.asset(
id: 'full_field',
label: 'Full field',
assetPath: 'assets/layouts/full_field.png',
aspectRatio: 16 / 9,
fit: BoxFit.fill,
)
The asset belongs to the host and must be declared by the host. Set package: only when the layout is shipped by another Flutter package.
Memory bytes
StrategyLayout.memory(
id: 'generated_layout',
label: 'Generated layout',
bytes: pngBytes,
aspectRatio: 4 / 3,
)
This accepts bytes already supplied by the host while constructing the configuration. Strategy Forge does not provide an upload or picker UI.
Any widget
StrategyLayout(
id: 'diagram',
label: 'Diagram',
aspectRatio: 2,
builder: (_) => const CustomPaint(painter: DiagramPainter()),
thumbnailBuilder: (_) => const Icon(Icons.map_outlined),
)
Layout IDs are serialized. Renaming or removing them makes existing documents incompatible unless the host migrates the JSON first.
Choose enabled tools #
Use the catalog list that matches the desired level of control:
| API | Contents |
|---|---|
StrategyToolCatalog.core |
One tool for each distinct editing behavior. |
StrategyToolCatalog.svgTools |
34 original SVG palette markers. |
StrategyToolCatalog.universalMarkerTools |
29 generic team, object, target, and training markers. |
StrategyToolCatalog.universalTools |
Current universal tool set; equivalent to universalMarkerTools. |
StrategyToolCatalog.all |
All 86 canonical entries in stable display order. |
StrategyToolCatalog.select(kinds) |
Core tools selected by StrategyToolKind. |
StrategyToolCatalog.selectByIds(ids) |
Any tools selected by canonical ID or supported alias. Unknown IDs throw ArgumentError. |
StrategyToolCatalog.tool(kind) |
One core tool for a behavior. |
StrategyToolCatalog.toolById(id) |
One catalog tool by canonical ID or alias. |
Example:
final tools = StrategyToolCatalog.selectByIds(const [
'select',
'freehand_arrow',
'dashed_stop_arrow',
'text',
'object_round_ball',
'target_goal',
]);
The package has no sport presets. Keep product-specific tool lists in the embedding app.
Keep hidden markers renderable #
An existing document can contain markers that are no longer offered for placement. Pass their definitions through markerRenderers:
final enabled = StrategyToolCatalog.selectByIds(const [
'select',
'arrow',
'text',
]);
final config = StrategyEditorConfig(
id: 'training_view',
name: 'Training view',
layouts: layouts,
tools: enabled,
markerRenderers: StrategyToolCatalog.all
.where((tool) => tool.kind == StrategyToolKind.marker)
.toList(),
);
This keeps historical markers visible without adding their tools to the toolbar.
Built-in tool catalog #
Groups with no enabled tools are omitted automatically. Core tools without an explicit group appear under Edit, Draw, Lines, Shapes, or Add. Bundled marker tools define their own group.
Core tools #
| Group | Stable IDs |
|---|---|
| Edit | select |
| Draw | freehand, freehand_arrow, freehand_dashed_arrow, freehand_stop_arrow, lateral, lateral_stop, backwards, wave, double_arrow, dashed_double_arrow |
| Lines | line, arrow, stop_arrow, dashed_line, dashed_arrow, dashed_stop_arrow |
| Shapes | rectangle, filled_rectangle, ellipse, filled_ellipse |
| Add | text, marker |
Original SVG marker palettes #
| Group | Stable IDs |
|---|---|
| Pucks | puck, puck_group |
| Equipment | equipment_cone, equipment_tire, equipment_stick, equipment_stickhandling, equipment_triangle, equipment_border_vertical, equipment_border_horizontal |
| Nets | net_up, net_down, net_right, net_left, net_mini |
| Players | player_forward, player_defense, player_opponent, player_type_1, player_type_2, player_type_x |
| Positions | position_center, position_left_wing, position_right_wing, position_left_defense, position_right_defense |
| Numbers | number_1 through number_9 |
Universal marker palettes #
| Group | Stable IDs |
|---|---|
| Teams | team_home, team_away, team_neutral, role_goalkeeper, role_official, role_substitute, role_coach, role_possession |
| Objects | object_round_ball, object_oval_ball, object_small_ball, object_disc, object_shuttlecock, object_curling_stone, object_bat, object_racket, object_broom |
| Targets | target_goal, target_hoop, target_net, target_wicket, target_base, target_bullseye, target_flag |
| Training | training_hurdle, training_pole, training_ladder, training_mannequin, training_zone |
Puck, stick, and cone remain in their original palettes to avoid duplicate tools with identical artwork.
Compatibility aliases #
Former duplicate IDs resolve to one canonical tool when using toolById, selectByIds, or canonicalIdFor:
| Former IDs | Canonical ID |
|---|---|
action_move, action_carry |
freehand_arrow |
action_pass, action_return |
dashed_arrow |
action_dribble, action_sweep |
wave |
action_shot, action_serve |
arrow |
action_screen |
stop_arrow |
action_press |
freehand_dashed_arrow |
action_rotate |
double_arrow |
forward_*, defense_* player variants |
Corresponding player_* ID |
object_puck |
puck |
object_stick |
equipment_stick |
training_cone |
equipment_cone |
position_goalie |
role_goalkeeper |
position_coach |
role_coach |
Use canonical IDs in newly persisted application data. Catalog aliases help with configuration code; arbitrary saved JSON is not rewritten automatically.
Editing behavior #
Drawing and line notation #
- Freehand tools follow the complete pointer path.
- Straight line tools use the gesture start and end positions.
- Solid and dashed variants have distinct rendering.
- Freehand dashed, straight dashed, lateral, and backwards notation retain their intended cadence and shape.
- Stop arrows combine an open arrowhead with stop bars.
- Two-way arrows place independently oriented heads at both rendered endpoints.
- Arrow direction is calculated from a short path sample to reduce pointer-jitter errors.
- Lateral arrows use the final rendered segment for arrowhead orientation.
- Outline and filled rectangles and ellipses use the drag bounds.
All document coordinates are normalized to the layout, so drawings scale with the available editor size.
Text #
Select text and tap the canvas to open the multiline editor. The built-in dialog accepts up to 240 characters. Select an existing text item and use its edit control to update the content. Text can also be moved, scaled, and rotated.
Selection and transforms #
Select select, then tap an element. The editor supports:
- dragging the selected item to reposition it;
- dragging a square corner handle to scale it without rotating;
- dragging the separate round handle to rotate it without scaling;
- pinch scaling on touch devices;
- 15-degree rotation buttons;
- 10% scale buttons, constrained to 25%–400%;
- an edit action when the selected element is text;
- move, resize, grab, and grabbing cursors on pointer platforms.
The selection frame follows the element's rotation. Transform changes enter the normal undo history.
History and clearing #
Layout changes and document edits are undoable. New edits clear the redo stack. loadDocument and loadJson start a fresh history. The built-in Clear button asks for confirmation and removes every element from the current document.
Icons, markers, and color behavior #
Every catalog tool has a default icon. The package includes its SVG assets and the MyFlutterApp and CustomIcon font families, so host apps do not need to copy those files.
Override one toolbar icon #
final arrow = StrategyToolCatalog
.tool(StrategyToolKind.arrow)
.withIconBuilder((_) => const Icon(Icons.star));
Use the returned tool in the configuration. iconBuilder can return any Flutter widget. When iconBuilder, icon, and icon SVG properties are absent, the package falls back to StrategyToolIcons.forKind(kind).
Define a custom marker #
const customGroup = StrategyToolGroup(
id: 'custom',
label: 'Custom',
icon: Icons.extension_outlined,
);
final checkpoint = StrategyTool(
id: 'checkpoint',
label: 'Checkpoint',
kind: StrategyToolKind.marker,
group: customGroup,
markerSize: 40,
iconBuilder: (_) => const Icon(Icons.location_on_outlined),
markerBuilder: (_) => const Icon(Icons.location_on, size: 40),
);
Custom marker IDs are serialized. Keep them stable and keep a renderer available for saved documents.
Use a custom SVG marker #
final marker = StrategyTool(
id: 'custom_target',
label: 'Custom target',
kind: StrategyToolKind.marker,
group: StrategyToolCatalog.targetsGroup,
markerSize: 44,
svgAssetPath: 'assets/markers/custom_target.svg',
primarySvgColor: const Color(0xffd94b4b),
);
Declare the SVG in the host app. Set svgAssetPackage if the file belongs to another package. iconSvgAssetPath and iconSvgAssetPackage can supply different toolbar artwork.
Selected color rules #
- A newly drawn stroke or text element stores the effective selected color.
- A newly placed marker stores an explicit color only when a palette color is selected.
- Choosing Default / Clear sets
selectedColortonull. Markers then keep their source artwork; drawing and text tools use the first configured palette color. - Bundled multicolor SVGs replace only the declared
primarySvgColor. Other colors, outlines, lettering, and highlights remain unchanged. - A custom SVG without
primarySvgColoris tinted as a complete image for compatibility. - A custom
markerBuilderis color-filtered as a complete widget when an explicit color is selected. - Changing the active color affects newly created elements. It does not rewrite existing document elements.
StrategyToolAssets exposes public constants for bundled SVG paths. StrategyToolIcons, MyFlutterApp, and CustomIcon expose the package icon APIs.
Controller API #
Create one StrategyEditorController per active editor view.
| Member | Purpose |
|---|---|
config |
Immutable configuration associated with the controller. |
document |
Current immutable StrategyDocument. |
toolId / tool |
Current tool selection. |
selectedColor |
Explicit color, or null when tool defaults are active. |
color |
Effective drawing color; falls back to the first configured color. |
strokeWidth |
Current configured line width. |
canUndo / canRedo |
History availability. |
selectTool(id) |
Activates a configured tool. |
selectColor(color) / clearColor() |
Changes or clears the explicit color. |
selectStrokeWidth(width) |
Activates a configured width. |
selectLayout(id) |
Changes the document layout and records history. |
addElement(element) |
Adds an element and records history. |
replaceElement(index, element) |
Replaces an element and records history. |
clear() |
Removes all elements and records history. |
undo() / redo() |
Navigates document history. |
loadDocument(document) |
Validates and loads a document, then clears history. |
exportJson() / loadJson(source) |
Serializes or restores a versioned document. |
exportPng(pixelRatio: 2) |
Captures the mounted editor content as PNG bytes. |
Selection methods validate their values against the configuration. Invalid tool IDs, layout IDs, colors, widths, and element indexes throw standard Dart errors.
Callbacks and host state #
The controller is a ChangeNotifier, so the host can convert document changes into callbacks without coupling the package to a state-management library. Controller notifications also occur for tool, color, and width selection; compare JSON to avoid redundant persistence writes:
String? lastExportedJson;
void emitDocumentIfChanged() {
final json = controller.exportJson();
if (json == lastExportedJson) return;
lastExportedJson = json;
onStrategyJsonChanged(json);
}
@override
void initState() {
super.initState();
controller.addListener(emitDocumentIfChanged);
}
@override
void dispose() {
controller.removeListener(emitDocumentIfChanged);
controller.dispose();
super.dispose();
}
Generate an image and forward it to a host callback:
Future<void> generatePreview() async {
final bytes = await controller.exportPng(pixelRatio: 2);
onStrategyImageGenerated(bytes);
}
The repository's DemoHome implements ready-to-use strategyJson, onStrategyJsonChanged, onStrategyImageGenerated, initialDownloadEnabled, and savePng properties.
JSON import and export #
Export and restore #
final source = controller.exportJson();
// Store `source` in the host app, then restore it later.
controller.loadJson(source);
The current schema version is 1:
{
"version": 1,
"configId": "training_view",
"layoutId": "full",
"elements": [
{
"type": "stroke",
"kind": "arrow",
"points": [[0.2, 0.3], [0.6, 0.5]],
"color": 4278190080,
"width": 4.0
},
{
"type": "text",
"position": [0.4, 0.2],
"text": "First line\nSecond line",
"color": 4278190080,
"fontSize": 16.0,
"rotation": 0.25,
"scale": 1.2
},
{
"type": "marker",
"position": [0.7, 0.6],
"toolId": "team_home",
"color": 4278233770
}
]
}
Schema details #
- Coordinates are normalized fractions of layout width and height.
- Colors are Flutter 32-bit ARGB integer values.
- Stroke
kinduses theStrategyToolKindenum name. - Text and marker positions use
[x, y]arrays. - Marker
toolIdidentifies the renderer. rotationis in radians and is omitted when zero.scaleis omitted when it equals1.- Marker
coloris omitted when the marker uses its original artwork.
Compatibility rules #
versionmust be1; another value throwsFormatException.configIdmust equal the controller configuration ID.layoutIdmust exist in the configuration.- Keep layout IDs, tool IDs, configuration IDs, and custom marker renderers stable.
- Use
markerRendererswhen loading documents that contain currently hidden placement tools. loadJsonreplaces the current document and clears undo/redo history.- Malformed or untrusted input can throw
FormatException,ArgumentError,TypeError, or lookup errors. Validate or catch errors at the host's storage/API boundary.
Treat the serialized document as package data. If an application must rename IDs or support a future schema, migrate a decoded JSON map in the host before calling loadJson.
PNG export #
final bytes = await controller.exportPng(pixelRatio: 3);
StrategyEditormust be mounted and bound to the controller.pixelRatiomust be greater than zero and defaults to2.- Higher values create larger images and use more memory.
- The PNG contains the background layout and rendered strategy elements.
- Built-in toolbars, selection handles, and transform controls are outside the captured repaint boundary.
- The package returns
Uint8List; the host decides whether to save, download, share, or upload it.
Custom controls #
Hide the package toolbar while retaining the canvas and controller behavior:
StrategyEditor(
controller: controller,
showControls: false,
)
Build app-specific controls around the public controller methods. The built-in control labels are currently English. Applications that require localized or substantially different controls can use this mode while keeping package drawing and export behavior.
Responsive and accessibility behavior #
The built-in interface adapts from its actual width and height constraints rather than device names or orientation. Tool rows scroll horizontally when needed; menus have bounded widths; short viewports use denser spacing; and the canvas preserves each layout's aspect ratio.
Controls use Material tooltips and semantic labels, and primary actions target at least 48 logical pixels. The host should still test the complete application with its chosen theme, text scale, layouts, enabled tool count, keyboard/mouse environment, and supported screen sizes.
Package assets #
The package declares:
assets/tools/for the original SVG artwork;assets/tools/universal/for reusable marker artwork;assets/fonts/MyFlutterApp.ttf;assets/fonts/CustomIcon.ttf.
Consumers receive these through the package dependency. Host-owned layouts and custom marker files remain in the host application's asset manifest.
Development and verification #
From this package directory:
flutter pub get
flutter analyze
flutter test
The package tests cover controller validation and history, JSON round trips, every drawing notation, endpoint arrowheads, text editing, independent selection transforms, responsive controls, icon overrides, primary SVG color replacement, asset packaging, and catalog integrity.
Run the example app tests as well when changing the public API or integration.
The package includes the complete demo as a standalone Flutter application in example. Run it with:
cd example
flutter pub get
flutter run
Report bugs and request features through the GitHub issue tracker.
Licensing #
Strategy Forge is open-source software released under the MIT License. You may use, copy, modify, merge, publish, distribute, sublicense, and sell copies subject to the license terms, including retaining the copyright and permission notices.
Third-party dependencies retain their respective licenses. Review Flutter, flutter_svg, and transitive dependency notices when distributing the package or an application that contains it.