🚧 These docs are a work in progress — content is incomplete and may change without notice.
Skip to content

Flow Arrow ​

The Flow Arrow (flow-arrow, 990003) is a solid arrow for illustrations whose body splits into separate flows that may merge again. Each flow ends in its own arrowhead, unless it merges into another flow or continues into flows that split from its tip. It is a generic graphic, not a doctrinal symbol.

The body is drawn as one outline: the union of a band along each flow plus its arrowhead. Where a flow splits off or merges, the outline has a clean edge and a sharp crotch. A flow that splits off one flow and merges into another makes a bypass, and the island it encloses is left open as a hole.

Flows in one control-point array ​

A Flow Arrow has one flat array of control points, like every other control measure. The flows option says how the array divides into flows:

ts
import { renderControlMeasure } from "@orbat-mapper/control-measures";

// The trunk runs east over points 0-3. Flow 1 (points 4 and 5) splits off
// the trunk at its vertex 2 and ends in its own arrowhead.
const render = renderControlMeasure({
  id: "main-effort",
  kind: "flow-arrow",
  controlPoints: [
    [10.0, 59.0], // trunk 0: rear
    [10.2, 59.0], // trunk 1
    [10.4, 59.0], // trunk 2: flow 1 splits here
    [10.6, 59.0], // trunk 3: tip
    [10.55, 59.08], // flow 1
    [10.7, 59.12], // flow 1: tip
  ],
  options: {
    flows: [
      { count: 4 },
      { count: 2, splitFrom: { flow: 0, vertex: 2 }, arrowheadStyle: "main-attack" },
    ],
  },
});
  • flows[0] is the trunk. The control points are the flows' own points in flows order, each running from its rear or split towards its tip or merge.
  • A split or merge point belongs to the flow it sits on and is stored only once. splitFrom and mergeInto name it as { flow, vertex }. Moving that point moves every flow attached to it.
  • A flow's vertex indices count its implicit split and merge points. In the example, flow 1's vertex 0 is trunk point 2.
  • A flow with mergeInto has no arrowhead and takes on its target's width where it meets it. A flow with both splitFrom and mergeInto is a bypass. A flow with neither has a rear of its own.
  • Without flows, every control point is one trunk.

Options ​

On the graphic:

OptionDescription
rearStyleHow every flow with its own rear ends behind: flat, notched, rounded, or open.
rearWidthRatioThe trunk's rear width.
arrowheadWidthRatio / arrowheadLengthRatioArrowhead size for every flow, as multiples of the flow's width where its arrowhead begins.
smoothCurve each flow through its points. A split flow leaves, and a merging flow arrives, along the other flow.
smoothResolutionHow finely a smooth flow is curved (2–64, default 12). Each segment gets as many points as its length and bend need, crowded into the bend; raise it for a finer curve.
filledFill the body solid; when false only the outline is drawn.

On each flow:

OptionDescription
arrowheadStyletriangle (default), main-attack, barbed, concave, diamond, harpoon, swallowtail, tee, flat, or open. An open end keeps its fill but has no stroke across it.
widthRatioThe flow's width where its arrowhead begins.
rearWidthRatioThe rear width of a flow with its own rear. Defaults to widthRatio.
arrowheadWidthRatio / arrowheadLengthRatioThis flow's arrowhead size, overriding the graphic's.
vertexLeftWidthRatios / vertexRightWidthRatiosPer-side widths at the flow's vertices, as on the variable-width arrows.

Widths are ratios of the trunk's length, measured along its control points, so a scaled Flow Arrow keeps its shape and turning smooth on or off never resizes it.

Split, merge, reverse and detach ​

Like the other arrows, a Flow Arrow is drawn tip first: the first click places the trunk's tip and later clicks lead back towards its rear. The control points are still stored rear to tip. Points added while drawing keep the width the first two clicks gave it: the draw scales the width ratios back as the trunk grows, as it does for the Block Arrow.

TacticalDraw edits a Flow Arrow with these gestures:

GestureResult
Ctrl/⌘ + drag from a pointPulls a new flow out of that point. Releasing on a ringed point of another flow makes the new flow a bypass.
Ctrl/⌘ + drag from a free tipMerges the tip onto a ringed point of another flow.
Drag a midpointAdds a point to that flow.
Alt + click a pointRemoves it. Points that carry a split or merge, and a flow's last spare point, stay.
Click a flow, or press one of its pointsSelects that flow. Its width and arrowhead grips appear.
Alt + click a gripResets the value behind it.

Ctrl/⌘ and Alt are the default keys; a host can change them with editModifiers.

While you drag a tip, TacticalDraw rings every point it may merge onto: points of another flow, outside any arrowhead, that do not make a flow depend on itself.

Three plain functions edit flows from a host's own UI. Each takes the control points, the options and a flow index, and returns new controlPoints and options, or null when the edit does not apply:

  • reverseFlow runs a flow the other way. A flow that split from another now merges into it, and the other way round.
  • detachFlowEnd gives a merging flow its own tip and arrowhead back.
  • removeFlow removes a flow and every flow that splits from it. Flows that merge into it are detached. The trunk can't be removed.

Apply the result in one updateGraphic call so it is one undo step:

ts
import { reverseFlow, type FlowArrowOptions } from "@orbat-mapper/control-measures";

const flow = session.selectedPart ?? 0;
const edit = reverseFlow(session.controlPoints, session.options as FlowArrowOptions, flow);
if (edit) session.updateGraphic((graphic) => ({ ...graphic, ...edit }));

The selected flow ​

Per-flow settings belong in a panel for the flow the user is working on. The edit session keeps that selected flow as session.selectedPart. It starts at the trunk, follows the user's clicks, and moves to a new flow pulled out with Ctrl/⌘ + drag. Read it to show the flow's settings, set it with session.selectPart(index), and follow it with session.onPartSelect():

ts
session.onPartSelect((flow) => showFlowPanel(flow));

function setArrowheadStyle(style: FlowArrowheadStyle) {
  const flow = session.selectedPart ?? 0;
  const flows = (session.options as FlowArrowOptions).flows ?? [];
  session.setOptions({
    flows: flows.map((f, i) => (i === flow ? { ...f, arrowheadStyle: style } : f)),
  });
}

Selecting a flow is view state: it emits no change event and records no undo step. If an edit or undo removes the selected flow, the selection falls back to the trunk.

Selecting several flows ​

To give several flows the same setting in one go, the session can hold a set of selected flows, session.selectedParts. selectedPart stays the primary flow, the one most recently added. Reshape mode shows the grips of every selected flow, and dragging a grip applies to the other selected flows too, as one undo step:

GripReaches
ArrowheadSelected flows with a head of their own.
WidthAll selected flows.
Rear widthSelected flows with their own rear, except the trunk, whose rear width is the graphic's and stays separate.
PointOnly its own flow.

Each flow gets the same values, computed from the grip you drag, so heads of different sizes snap together when the drag starts. Arrowhead sizes are ratios of each flow's width, so heads on flows of different widths match in proportion, not in absolute size. A rear with its own right-side width takes the drag on that flow alone.

Alt+click on a grip clears what that grip resets on the flows it reaches: an arrowhead grip clears their arrowhead sizes, and a rear width grip their right-side rear widths. A width grip has nothing to reset.

ts
session.selectPart(2, { extend: true }); // add flow 2, or remove it if selected
session.selectParts([0, 1, 2]); // "All flows"
session.onPartSelect((flow, flows) => showFlowPanel(flows));

On the map, give the extendPartSelection modifier key a key, such as Shift. A click on a point, midpoint or the body with that key held adds the flow to the set or removes it; a plain click selects the flow alone. Dragging a point of a flow that is already selected keeps the whole selection, so you can reshape one flow without losing the others. The set always keeps one flow: removing the primary promotes the flow added before it, and the last flow can't be removed.

Flows are identified by index, so removing a flow renumbers the ones after it. Whenever an edit or undo changes the number of flows, the set collapses to its primary flow (or to the trunk if the primary went away), so a selected index never ends up naming a different flow.