# Writing an Auto Shape for Editorial

This is the customer guide to writing a `.shape` script for Editorial's Auto Shapes.
It is written so that someone (or an AI agent) with **no access to Editorial's source
code** can author a correct, working shape from this document alone. The rules,
limits and defaults below describe Editorial itself.

An Auto Shape is a parametric drawing: a small script that, given a handful of
persistent numbers (the *parameters*), generates paths, text and drag handles. The
user drags the handles; the script regenerates. The shape lives in the picture as a
normal object that can be moved, scaled and rotated, and each part can be recoloured.

Put the script in the `smartshapes` folder beside `Editorial.exe`. The file name
becomes the shape's name in the picker: underscores and hyphens become spaces, so
`smart_polygon.shape` shows as "Smart polygon". Add a PNG with exactly the same
file stem (`smart_polygon.png`) for the picker. Use a 128 by 128 transparent image
with black artwork, safe padding around the edges, and the same simple monochrome
style as Editorial's toolbar icons. If the matching icon is absent, Editorial uses
`default.png`.

Keep the `.shape` filename stable after distributing it. An Editorial document saves
the generated artwork and remembers the script by filename, so it can still display
the object when that script is absent. The matching script must be installed under
the same filename, however, to edit its custom handles or regenerate it. Give the
`.shape` file and PNG to collaborators who need to edit the custom shape.

---

## 1. The `.shape` file and its sections

A `.shape` file is plain UTF-8 text divided into sections. A section starts with a
header line `[sectionname]` in square brackets at the start of a line; everything
until the next header belongs to it.

```
[name]
Talking Balloon

[description]
A speech balloon with a movable tail.

[init]
shape.parameter("radiusX", 140, 55)
...

[draw]
...
```

Sections:

| Section | Required | When it runs | Purpose |
|---|---|---|---|
| `[name]` | no | never (read as text) | a human-readable script title; defaults to `Unnamed shape` and does not control the picker name |
| `[description]` | no | never | free text for humans; ignored by Editorial |
| `[init]` | **yes** | whenever Editorial prepares the shape for drawing or interaction | declare every parameter |
| `[draw]` | **yes** | after every change | generate all paths, text and handles |
| `[drag:HANDLE]` | for each draggable handle | while the user drags that handle | update parameters from the pointer |
| `[click:HANDLE]` | for each click handle | when the user clicks that handle | update parameters on a click |
| `[insert]` | no | once at creation, only if the user *dragged* to create | map the creation drag onto parameters |

`HANDLE` is the name you give a handle with `shape.handle(...)`. A shape with a
handle named `tail` and a section `[drag:tail]` lets the user drag that handle.

A script **must** contain `[init]` and `[draw]` or it will not load. Section and
handle names are case-sensitive: `[drag:tail]` does not match a handle named `Tail`.
Use one occurrence of each section rather than splitting one section across the file.

The order of code inside a section is top to bottom. Editorial starts an evaluation
with `[init]`, then runs the requested `[insert]`, `[drag:*]` or `[click:*]` section
when there is one, and finally runs `[draw]`. Editorial may evaluate the same object
many times, so `[init]` is **not** a once-per-object event. Keep it idempotent and use
it to declare parameters, not to perform one-time work. `[draw]` must recompute
everything from the parameters; it must not rely on anything an earlier section left
in a local variable. Locals do not survive between section runs — only parameters do.

---

## 2. Parameters: the only persistent state

A parameter is a named number that persists across runs. Declare each one in
`[init]`, and read it wherever you need it:

```
shape.parameter(name, default [, minimum [, maximum]])
```

- `name` — a string, e.g. `"radiusX"`.
- `default` — the value a new shape starts with.
- `minimum`, `maximum` — optional. **Every read re-clamps** the stored value to the
  range you pass in *that* call. Omit them for no bound.

`shape.parameter` both **declares** (first time, in `[init]`) and **reads** (every
later call returns the stored value, clamped). So the same line works in `[init]`
and `[draw]`:

```
[init]
shape.parameter("radiusX", 140, 55)     // declare with a default and a floor

[draw]
let rx = shape.parameter("radiusX", 140, 55)   // read it back, still clamped to >= 55
```

To change a parameter (only meaningful in `[drag:*]`, `[click:*]`, `[insert]`):

```
shape.setParameter(name, value)         // clamps to the range last declared for `name`
```

Spell the name exactly as it appears in `[init]`. If `setParameter` is given an
undeclared name, Editorial creates a new unbounded parameter instead of reporting a
spelling error. The intended parameter will then remain unchanged. A final review of
all parameter names is therefore important.

**Ranges and node freedom.** Positional parameters (an X, a Y, a point the user
drags anywhere) should carry **no range at all** — pass only the default:

```
shape.parameter("tailX", 148)           // free to go anywhere
```

Give ranges only to counts, ratios, angles and minimum sizes:

```
shape.parameter("sides", 7, 3, 25)          // a count
shape.parameter("innerRatio", 0.5, 0, 1)    // a ratio
shape.parameter("radius", 120, 20)          // a size with a sensible floor, no ceiling
```

A range tied to the canvas (`shape.width - 20`) fences the handle inside the shape's
box and is almost always wrong. The logical canvas only sets the default layout (see
below); the user scales the whole object with the selection box.

---

## 3. The coordinate system

The script draws on a **logical canvas** whose size is `shape.width` by
`shape.height`. These are read-only:

```
let cx = shape.width / 2
let cy = shape.height / 2
```

Coordinates are plain numbers in that canvas. **Y increases downward** (screen
convention): `(0,0)` is the top-left, `(shape.width, shape.height)` the bottom-right.
An angle of `0` points right (+X); `+pi/2` points down.

Whatever you draw, the shape is then placed in the picture and can be scaled and
rotated by the user — you never handle that. New Editorial shapes use a 400 by 300
logical canvas. A creation drag changes the object's placement and transform; it does
not change `shape.width` or `shape.height`. Draw at a natural size around the canvas
centre and allow intentional controls, such as free-point handles, to move beyond the
canvas edges.

These read-only properties describe the current evaluation:

- `shape.pointerX`, `shape.pointerY` — the pointer in logical canvas coordinates
  during `[drag:*]`, or the release point during `[insert]`.
- `shape.dragStartX`, `shape.dragStartY` — the press point during `[insert]`.
- `shape.dragged` — meaningful in `[insert]`; it is `true` because `[insert]` runs
  only for a creation drag.
- `shape.operation` — `"init"`, `"draw"`, `"drag"`, `"click"` or `"insert"` for
  the section Editorial is evaluating.

Click callbacks do **not** receive the mouse position: `shape.pointerX` and
`shape.pointerY` are zero in `[click:*]`. Use a drag handle when position matters and
a click handle only for a discrete action.

---

## 4. Drawing API

Call these on `shape` inside `[draw]` (only). Colours are strings `"#RRGGBB"` or
`"#AARRGGBB"` (A = alpha/opacity), or `"none"` for no fill / no outline.

### Paths (the main way to draw)

```
shape.beginPath(fill, stroke, strokeWidth [, cap])   // start a path
shape.moveTo(x, y)                                    // pen up, then down at (x,y)
shape.lineTo(x, y)                                    // straight segment to (x,y)
shape.curveTo(c1x, c1y, c2x, c2y, endx, endy)         // cubic Bezier: two controls, end
shape.closePath()                                     // close back to the last moveTo
shape.endPath()                                       // finish this path
```

- `fill` / `stroke` — a colour or `"none"`. `strokeWidth` — a number (0 allowed).
- `cap` — optional line-cap for open strokes: `"butt"` (default), `"round"` or
  `"square"`. Use `"round"` for a smooth pen-like line; do **not** paste a dot on the
  end of a line to fake a round cap (it would take its own colour).
- One `beginPath ... endPath` is one path. You can draw many paths in one `[draw]`.
- A cubic is **three** numbers-pairs: the two control points then the end point.
- Line joins are mitred and cannot be changed by the script. Avoid thick outlines on
  very acute corners because a mitred join can form a long spike.

Every shape must generate **at least one path**. A script that creates only text and
handles is rejected as an empty shape; add an appropriate visible or transparent path
that represents the object.

**A filled open path** fills as if closed by a straight line from the last point back
to the start, but the outline stays open (it does not draw that closing edge). To get
a closed outline, call `closePath()`.

Example, a triangle:

```
shape.beginPath("#5B9BD5", "#1F4E79", 1.5)
shape.moveTo(50, 10)
shape.lineTo(90, 90)
shape.lineTo(10, 90)
shape.closePath()
shape.endPath()
```

### Rectangles and ellipses (shorthands)

```
shape.rect(x, y, width, height, fill, stroke, strokeWidth)
shape.ellipse(x, y, width, height, fill, stroke, strokeWidth)
```

`x, y` is the top-left corner; `width, height` the size. The ellipse is inscribed in
that box. These are complete paths on their own (no begin/end needed).

### Text

```
shape.text(text, x, y, width, height, fontSize, color, alignment, fontFamily, bold)
```

- `text` — the string to show. `x, y, width, height` — the box it sits in.
- `fontSize` — points, clamped 1..512. `color` — a colour (`"#RRGGBB"`).
- `alignment` — `"left"`, `"center"` or `"right"`.
- `fontFamily` — e.g. `"Segoe UI"`. `bold` — `true` or `false`.

Editorial draws all paths first and all text afterwards. Text therefore appears above
every path, regardless of the order of `shape.text` and path calls in `[draw]`. Later
paths appear above earlier paths, and later text appears above earlier text.

Build text with the string tools (see the language section):

```
shape.text("Day " & str(day), 10, 10, 80, 20, 14, "#000000", "center", "Segoe UI", true)
```

### Handles (what the user grabs)

```
shape.handle(name, x, y, tooltip [, action [, color]])
```

- `name` — the handle's id; it selects which `[drag:name]` / `[click:name]` runs.
- `x, y` — where the handle sits (draw it at the point it controls).
- `tooltip` — the text shown on hover. Keep it short and say what dragging does.
- `action` — `"drag"` (default) or `"click"`. A `"drag"` handle runs `[drag:name]`
  on every mouse move; a `"click"` handle runs `[click:name]` once on release.
- `color` — optional tint for the node, `"#RRGGBB"` or `"#AARRGGBB"`. Leave it out
  and the node is Editorial's usual one, which is what nearly every shape wants.
  Editorial draws the node's outline by darkening whatever fill you give, so you
  name one colour, not two, and every tinted node looks like it belongs.

Colour handles only when a shape has **several kinds of control** and the user
would otherwise have to hunt: the 3D box tints its three rotation rails alike, its
perspective rail differently, and its reset button differently again, so the eight
nodes read as three groups instead of eight identical dots. A shape with one kind
of control gains nothing from it. Do not use it to mark a handle as important, and
do not try to match the artwork's colours: a node sitting on artwork of its own
colour disappears.

Handles are the shape's whole interaction. Put one at every point the user should be
able to change. A slider is just a handle you constrain to one axis in its drag
section (see the polygon example). Give every handle a unique, case-sensitive name.
Handles are editing controls; they are not part of the artwork or exported image.

### Segments (independently recolourable parts)

Wrap drawing in a named segment so the user can pick and recolour that part alone:

```
shape.beginSegment("name")
  ... paths / text for this part ...
shape.endSegment()
```

Give each visually distinct part its own segment name (`"cloud"`, `"dots"`,
`"hourHand"`, `"section3"`). Untagged drawing still shows but cannot be recoloured on
its own. When the user recolours a segment, the choice is remembered and re-applied
to that segment on the next regeneration, so you do not manage colours yourself — draw
each segment in its natural colour and Editorial applies the user's override.

A segment name must be non-empty, no longer than 128 characters, and contain only
letters, digits, underscores and hyphens. Two *different* parts must not share a
name, because the user's colour is stored against the name. Reopening one name is
fine, though: a segment is a tag on each thing you draw rather than a container,
so a part may be drawn in several passes (the calendar's header does this), and a
part that draws nothing this time still keeps its colour for the next time. Names are case-sensitive. Invalid
names silently produce untagged artwork. Segments are flat rather than nested: always
finish the current path and call `shape.endSegment()` before beginning another
segment. If a segment contains both paths and text, test its Fill and Line recolouring
in Editorial to confirm that each drawn aspect changes as intended.

**Keeping a part's text colour (`shape.keepColor()`).** When the user selects the
whole shape and applies a fill, a text-only segment's labels normally follow that
fill (correct in general: a text and a shape recoloured together should match). If a
segment's text should instead keep its own colour on a *whole-object* recolour — the
numbers on a clock face, say — call `shape.keepColor()` inside that segment:

```
shape.beginSegment("numerals")
shape.keepColor()            // the numbers keep their colour when the whole clock is recoloured
shape.text(str(n), ...)
shape.endSegment()
```

This is opt-in and narrow: it holds only that segment's **text**, and only when the
whole object is recoloured. Its shapes still recolour, and if the user picks that
segment on its own and applies a colour, the text still changes — a deliberate
recolour always wins.

**Letting the user type over a label (`shape.editableText()`).** By default every
label is computed by the script and cannot be edited — resizing a measure tool
recomputes its distance, so a typed value would be lost. Mark a segment's label
`editableText()` when the user should be able to replace it with their own text (a
dimension you want to read "500 mm" instead of the raw pixel span, a callout you want
to word yourself):

```
shape.beginSegment("text")
shape.editableText()         // the Text tool can type over this label
shape.text(str(distance) & unit, ...)
shape.endSegment()
```

With the Text tool, clicking that label opens a small edit box over it; what the user
types is remembered and shown in place of the script's text, and it survives resizing
and saving. Clearing the text (leaving the box empty) reverts the label to the script
value. The override is per segment, so only put one editable label in a segment. It is
independent of `keepColor()`; a label can use either, both, or neither.

---

## 5. The scripting language

The language is a small BASIC-like language. Case-sensitive. One statement per line;
newlines end statements. Comments start with `//`.

This is the **same** language Editorial runs everywhere else, so its full reference,
`editorial_script_language_syntax.md`, describes the same syntax and the same
built-in functions down to the token. The one difference is what a script can reach:
an Auto Shape script sees the `shape`, `math` and `date` objects documented below, and
**not** the document-side objects that reference also covers (`document`, `llm`,
`promptFlow`, `input`, `TEXT` / `SUMMARY` / `LORE` / `OUTLINE` / `NOTES`) — those exist
only when a script runs against a document, not a shape. Everything in this section is
enough to write a shape on its own; the built-in lists below are the ones a shape
commonly needs, a curated subset of the full set in that reference (which all works
here except those document objects).

### Variables

```
let x = 10                 // declare a local (lives only within this section run)
x = x + 1                  // assign
```

Locals do **not** persist between section runs. Parameters are the only persistent
state.

### Numbers, strings, booleans, arrays

```
let n = 3.5
let s = "hello"
let ok = true              // also false
let arr = ["a", 2, true]   // arrays may contain mixed value types
let v = arr[0]             // index (0-based)
```

Strings join with `&`; turn a number into a string with `str(...)`:

```
let label = "Item " & str(n)
```

Inside a quoted string, use `\"` for a quote, `\\` for a backslash, and `\n`,
`\t` or `\r` for newline, tab or carriage return. Triple quotes hold multiline text:

```
let note = """First line
Second line"""
```

### Control flow

```
if x > 5 then
    y = 1
else
    y = 2
endif
```

`if` requires `then`; close with `endif`. `else` is optional. Conditions use
`== != <> < <= > >=` and `and` / `or` / `not`; `!=` and `<>` both mean not equal.

```
for i = 0 to count - 1
    ... use i ...
next
```

`for` counts inclusive from the first to the second value. Add `step`:

```
for i = 10 to 1 step -1
    ...
next
```

`step 0` is an error. `next` may be bare or `next i`. `break` exits the nearest loop;
`continue` skips to the next iteration.

Use `for value in array` to visit each item in an array:

```
for value in arr
    ... use value ...
next
```

```
while condition
    ...
wend
```

### Subroutines

Define reusable code with `sub ... end sub`. A subroutine can call the `shape` API.
It has its own local scope; to touch a top-level variable, declare it `global`
inside the sub:

```
sub arc(cxp, cyp, r, a0, sweep)
    global cx      // if the sub needs a top-level variable, import it
    let k = 4 / 3 * math.tan(sweep / 4)
    ...
    shape.curveTo(...)
end sub

arc(cx, cy, 80, 0, math.pi / 2)   // call it
```

A subroutine can return a value and be used in an expression:

```
sub doubled(value)
    return value * 2
end sub

let width = doubled(12)
```

Built-in scope: `shape`, `math`, `date` and the built-in functions are always
visible inside subs without `global`.

### Reserved words (cannot be variable names)

`for next in to step while wend if then else endif let print stop break continue sub
end return global and or not true false null`. Notably `step` and `end` are reserved,
so do not name a variable `step`.

### Math

`math` provides:

```
math.pi
math.sin(a)  math.cos(a)  math.tan(a)  math.atan2(y, x)
math.sqrt(x)   // x is floored at 0
math.round(x)  math.floor(x)  math.ceil(x)
math.pow(a, b)  math.mod(a, b)
```

Angles are radians. Also available as plain functions (no `math.`): `abs(x)`,
`min(a, b)` and `max(a, b)`.

There is **no clamp function**. Clamp by nesting the two you have, which is what
every sample does:

```
let y = min(max(shape.pointerY, cy - r), cy + r)
```

Do not reach for `between` here. It exists, but it is a **string** helper,
`between(text, openTag, closeTag)`, which returns the text between two markers;
given numbers it returns an empty string and your coordinate silently becomes
nothing.

### Useful string / array functions

Common functions and their argument order are:

```
str(value)                         int(value)
len(stringOrArray)
append(array, value)               // returns a new array
split(string, delimiter)           join(array, delimiter)
upper(string)                      lower(string)       trim(string)
contains(string, substring)        indexOf(stringOrArray, value)
left(string, count)                right(string, count)
mid(string, start [, count])       // start is 1-based
repeat(string, count)
padLeft(string, width [, char])    padRight(string, width [, char])
```

More of the same family exist and work in a shape — `startsWith`, `endsWith`,
`before`, `after`, `beforeLast`, `afterLast`, `count`, `lastIndexOf`, `lines`,
`ltrim`, `rtrim`, `titleCase`, the `_nocase` variants — see the full reference for
the complete list and exact arguments. Prefer the ones above unless you need one of
these; a shape rarely does.

Array indexing and the index returned by `indexOf` are zero-based. `append` does not
change its input array, so keep its returned value:

```
marks = append(marks, angle)
```

### Dates (for calendar-like shapes)

`date` provides the current `date.year`, `date.month`, `date.day`, `date.hour`,
`date.minute`, and the helpers `date.daysInMonth(year, month)` and
`date.dayOfWeek(year, month, day)`.

---

## 6. Interaction sections

### `[drag:NAME]`

Runs while the user drags the handle named `NAME`. Read `shape.pointerX` /
`shape.pointerY` and set parameters. Do not draw here.

A free point handle just follows the pointer:

```
[drag:tail]
shape.setParameter("tailX", shape.pointerX)
shape.setParameter("tailY", shape.pointerY)
```

A radius handle uses the distance from the centre:

```
[drag:radius]
let cx = shape.width / 2
let cy = shape.height / 2
let dx = shape.pointerX - cx
let dy = shape.pointerY - cy
shape.setParameter("radius", math.sqrt(dx * dx + dy * dy))
```

A vertical slider maps the pointer's Y to a count:

```
[drag:sides]
let cy = shape.height / 2
let r = shape.parameter("radius", 120, 20)
let y = min(max(shape.pointerY, cy - r), cy + r)     // clamp to the slider's span
shape.setParameter("sides", math.round(3 + 22 * (cy + r - y) / (2 * r)))
```

### `[click:NAME]`

Runs once when the user clicks a `"click"` handle. Use it for discrete actions —
toggles, steppers, month buttons:

```
[click:split]
let s = shape.parameter("split", 0, 0, 1)
if s > 0.5 then
    shape.setParameter("split", 0)
else
    shape.setParameter("split", 1)
endif
```

There are no modifier keys (no Shift/Ctrl/Alt in scripts). Where a desktop tool used
a modifier, use a separate click handle instead.

### `[insert]`

Optional. Runs once, at creation, only when the user *dragged* to create the shape.
Map the drag onto parameters. Typical for line-like shapes (an arrow, a measure line):
the shape runs from where the drag began to where it ended.

```
[insert]
shape.setParameter("fromX", shape.dragStartX)
shape.setParameter("fromY", shape.dragStartY)
shape.setParameter("toX", shape.pointerX)
shape.setParameter("toY", shape.pointerY)
```

If a script has `[insert]`, Editorial treats a creation drag as a from/to gesture and
shows a live preview of the generated shape. Without `[insert]`, a drag fits the
400-by-300 logical canvas into the dragged box. A plain click always places the shape
at its default size and does not run `[insert]`.

---

## 7. Colour conventions

- **Solid shapes** (a star, a polygon, an arrowhead, a filled blob) should have a
  fill and **no outline**: `shape.beginPath("#F2B632", "none", 0)`. Editorial then
  lets either palette button recolour the fill.
- **Lines and open strokes** have a stroke and **no fill**:
  `shape.beginPath("none", "#313131", 2, "round")`.
- **A part with both** (an area with a border) has both a fill and a stroke.
- Text colour is the text's ink.
- Editorial never *adds* an aspect a part did not draw: a line stays a line, a solid
  shape stays outline-free, however the user recolours it. So decide fill-vs-stroke
  when you draw, per part.
- A "hole" in a filled shape is a second subpath drawn the **opposite direction**
  (if the outer ring goes clockwise, the hole goes counter-clockwise), so it reads as
  a hole under any fill rule. See the cog and thinking-bubble samples.

---

## 8. Robustness and limits

Scripts run synchronously on Editorial's user-interface thread and must not hang.
Editorial enforces:

- **Script file size** capped at 4,194,304 bytes.
- **100,000 executed statements** per section run. A runaway loop stops with an error.
- **String length** capped at 4,194,304 characters (`&`, `repeat`, `padLeft`...).
- **Text element length** capped at 32,768 characters.
- **Subroutine call depth** 100 (unbounded recursion is stopped, not a crash).
- **Nesting** of parentheses / blocks 200 (a parse error past that).
- Recorded output caps: **4,096 paths**, 100,000 commands per path, **1,024 handles**,
  **4,096 text elements**. Calls past a cap are silently dropped, so keep loops sane
  (a star with `points` up to a few dozen, not thousands).

Beyond the hard limits, keep `[draw]` cheap: it runs on every mouse move during a
drag. Avoid per-frame work that grows without bound (an O(n^2) loop over a
user-controlled count). If you must walk a curve to place things evenly, do it with a
fixed number of steps (e.g. 24), not one proportional to a large parameter.

Guard against degenerate maths: `math.sqrt` floors its input at 0 for you, but you
should clamp divisions (`let d = max(1, length)`) and keep radii and sizes above a
small floor so the shape never collapses or self-intersects.

Handles, rectangles, ellipses and individual path commands that receive a non-finite
number (NaN or infinity) are ignored silently. A division by zero in a coordinate can
therefore remove a command or element without an error, leaving broken-looking
artwork. Prevent it at the source.

---

## 9. A complete minimal example

A resizable rounded triangle with one segment and two handles: a size handle and a
click handle that cycles the fill through three colours.

```
[name]
Triangle

[description]
A simple worked example for the authoring guide.

[init]
// Positional parameters carry no range; the count/choice ones do.
shape.parameter("radius", 90, 20)
shape.parameter("colour", 0, 0, 2)

[drag:size]
let cx = shape.width / 2
let cy = shape.height / 2
let dx = shape.pointerX - cx
let dy = shape.pointerY - cy
shape.setParameter("radius", math.sqrt(dx * dx + dy * dy))

[click:colour]
let c = math.round(shape.parameter("colour", 0, 0, 2)) + 1
if c > 2 then
    c = 0
endif
shape.setParameter("colour", c)

[draw]
let cx = shape.width / 2
let cy = shape.height / 2
let r = shape.parameter("radius", 90, 20)
let choice = math.round(shape.parameter("colour", 0, 0, 2))
let fill = "#5B9BD5"
if choice == 1 then
    fill = "#F2B632"
endif
if choice == 2 then
    fill = "#E5252A"
endif

// Three points at -90, 30, 150 degrees.
let a = 0 - math.pi / 2
shape.beginSegment("body")
shape.beginPath(fill, "none", 0)
shape.moveTo(cx + math.cos(a) * r, cy + math.sin(a) * r)
for i = 1 to 2
    let b = a + i * 2 * math.pi / 3
    shape.lineTo(cx + math.cos(b) * r, cy + math.sin(b) * r)
next
shape.closePath()
shape.endPath()
shape.endSegment()

// Handles: a size handle on the top point, a click handle to cycle the colour.
shape.handle("size", cx + math.cos(a) * r, cy + math.sin(a) * r, "Size")
shape.handle("colour", cx, cy, "Click to change the colour", "click")
```

---

## 10. Pitfalls (every one of these was a real bug while building the samples)

These are not hypothetical. Each cost time during development; a new author will hit
them too. Read this section before writing, not after debugging.

### 10.1 Comments are `//`, not `'`

BASIC uses `'` for comments; this language does **not**. A leading `'` is a parse
error ("Unexpected token: '''").

```
' wrong: this is a parse error
// right
```

### 10.2 `step` and `end` are reserved — do not use them as variable names

Naming a loop's stride `step` gives "Unexpected token: 'step'". This bit us in the
cloud generator. Rename it.

```
// wrong
let step = d / pieces
// right
let sweep = d / pieces
```

The full reserved list is in section 5.

### 10.3 Locals do not survive between sections

`[draw]` runs fresh every time. A value you compute in `[drag:tail]` is gone when
`[draw]` runs. The **only** channel between sections is parameters. If `[draw]` needs
a number, it must read it from a parameter, not assume a drag left it in a local.

```
// wrong: nodeAngle set in [drag:x] is undefined in [draw]
// right: store it
[drag:tailAngle]
shape.setParameter("tailAngle", ...)
[draw]
let a = shape.parameter("tailAngle", 0.9, -math.pi, math.pi)
```

### 10.4 `let` inside a block does not outlive the block

An `if`, `for` or `while` body is its own scope. A variable **declared** there is
gone at `endif` or `next`. Assigning one that already exists outside does reach
out and change it, which is the pattern you want: declare before the block, assign
inside it.

```
// wrong: "size" does not exist after the endif
if lengthSquared < 0.01 then
    let size = 10
else
    let size = reading
endif
shape.setParameter("width", size)     // undeclared variable 'size'

// right: declare outside, assign inside
let size = 10
if lengthSquared >= 0.01 then
    size = reading
endif
shape.setParameter("width", size)
```

### 10.5 A range tied to the canvas fences the handle

If a draggable point's parameter is clamped to the canvas, the user cannot drag it
past the shape's own edge — the classic "the node stops at the boundary" bug.
Positional parameters take **no** range.

```
// wrong: the tail can never leave the box
shape.parameter("tailX", 148, 20, shape.width - 20)
// right
shape.parameter("tailX", 148)
```

### 10.6 A single cubic cannot bend more than about a quarter turn

Approximating a circular arc with one `curveTo` over more than ~90 degrees bulges,
and near a half turn it folds back over itself. Split a big arc into quarter-turn
pieces. This is why the cog and cloud draw arcs in a loop.

```
sub circleArc(ox, oy, r, a0, d)
    let pieces = math.ceil(abs(d) / (math.pi / 2))
    if pieces < 1 then
        pieces = 1
    endif
    let sweep = d / pieces
    let k = 4 / 3 * math.tan(sweep / 4)     // the cubic-arc constant for this sweep
    for p = 0 to pieces - 1
        let t0 = a0 + sweep * p
        let t1 = t0 + sweep
        let x0 = ox + r * math.cos(t0)
        let y0 = oy + r * math.sin(t0)
        let x1 = ox + r * math.cos(t1)
        let y1 = oy + r * math.sin(t1)
        shape.curveTo(x0 - k * r * math.sin(t0), y0 + k * r * math.cos(t0), x1 + k * r * math.sin(t1), y1 - k * r * math.cos(t1), x1, y1)
    next
end sub
```

### 10.7 Equal *parameter* steps are not equal *distance* along a curve

Placing things at `t = i/n` along a Bezier bunches them where the curve is slow (near
a strong bend). The thinking-bubble's dots and scallops both looked wrong until they
were spaced by **arc length** instead. If even spacing matters, walk the curve once in
a fixed number of small steps, accumulate distance, and place marks at equal distance
fractions. Do the walk **once** and read positions from it (see 10.12), never re-walk
per item.

### 10.8 Do a fixed amount of work in `[draw]`, not work proportional to a big count

`[draw]` runs on every mouse move. An arc-length walk that re-walks the curve for
each of N dots is O(N x steps) per frame and made the cloud visibly lag. Walk once,
cache the marks in an array, then place from the array.

### 10.9 Solid shapes must have `"none"` for the outline

Give a filled shape a stroke and the Line-width slider thickens that stroke; a mitred
corner then spikes past the shape (the arrow-head "stub" bug). Solid shapes are fill
only.

```
// wrong: a star that grows a spiky outline
shape.beginPath("#F2B632", "#875A00", 1.5)
// right
shape.beginPath("#F2B632", "none", 0)
```

### 10.10 A line must stop *inside* a solid end, not run to the tip

A shaft that runs to the very tip pokes out the front of a solid arrowhead. Stop the
shaft short by the head's depth (half way into an arrowhead, at the centre of a circle
or square end), and give it a real round cap rather than a pasted-on dot.

```
let shaftEnd = tipX - direction * headLength * 0.5   // inset the shaft under the head
shape.beginPath("none", colour, lineWidth, "round")
shape.moveTo(fromX, fromY)
shape.lineTo(shaftEndX, shaftEndY)
shape.endPath()
```

### 10.11 A hole must wind the opposite way

Drawing an inner ring the same direction as the outer one fills it in. To cut a hole,
draw the inner subpath the other way round (reverse the loop). The cog's centre hole
and the closed polygon's inner ring both do this.

```
// outer ring, forwards
for i = 0 to n - 1
    shape.lineTo(cx + R * math.cos(a0 + i * da), cy + R * math.sin(a0 + i * da))
next
shape.closePath()
// hole, backwards
for i = n - 1 to 0 step -1
    shape.lineTo(cx + r * math.cos(a0 + i * da), cy + r * math.sin(a0 + i * da))
next
shape.closePath()
```

### 10.12 Cache a one-pass computation in an array; do not recompute per item

```
// walk once, collect the angle at each mark
let marks = []
let acc = 0
... one loop over the curve, marks = append(marks, angle) at each target distance ...
// then use marks[i] in the drawing loop -- no second walk
```

### 10.13 Non-finite coordinates vanish silently

A division by zero or another calculation that produces infinity or NaN can make an
individual path command or handle disappear with no error — the shape just looks
broken. Clamp denominators (`let d = max(1, length)`) and keep sizes above a floor.

### 10.14 Keep coupled counts in proportion

When one count depends on another (the polygon's "sections" of its "sides"), changing
the first while the second stays fixed leaves gaps. When you change the driving count
in a drag section, rescale the dependent one to keep the ratio.

### 10.15 Do not add a handle that turns the finished picture

The user already rotates the whole object with the selection box. A handle that just
spins the drawing is redundant and collides with the radius/size handles (the star had
this). Draw the shape in a fixed orientation and let the object rotate.

The test is whether the selector box could do the same thing. An angle that only turns
the finished picture: leave it out. An angle the drawing is genuinely built from, which
no flat rotation could produce, is a parameter like any other and deserves its handle.
The polygon's rotation only decides where its first corner sits; the 3D box's three
angles change which faces you can see at all.

### 10.16 `&` joins strings, `str()` converts a number

```
// wrong: this adds numbers or errors
shape.text("Day " + day, ...)
// right
shape.text("Day " & str(day), ...)
```

### 10.17 A mitred join spikes when a part can turn edge-on

Joins are mitred and a script cannot change that. The spike a mitre throws grows
as one over the sine of half the corner angle, so it is unbounded: as a corner
closes toward zero degrees the join fires a long spear out past the artwork, and
it gets longer the wider the user sets that line. The 3D box shipped with this.
Its edges were one closed outline per face, and a face turned nearly edge-on has
corners near zero, so spears shot out of the box at those angles and vanished
again the moment the face went fully edge-on and stopped being drawn.

This is not a case for clamping the angle. Stroke the outline as **separate
two-ended lines with round ends**, which have no join to spike at any width:

```
// wrong: one closed outline, mitred corners, spikes when the quad goes thin
shape.beginPath("none", ink, width)
shape.moveTo(x0, y0)
shape.lineTo(x1, y1)
shape.lineTo(x2, y2)
shape.closePath()
shape.endPath()

// right: one path per edge, round ends, nothing to spike
shape.beginPath("none", ink, width, "round")
shape.moveTo(x0, y0)
shape.lineTo(x1, y1)
shape.endPath()
```

Ask whether any corner of the part can close up as the user drags it. A fixed
polygon is safe; anything that turns, flattens or foreshortens is not. Listing
the edges also lets you draw each one once, where outlining every face draws a
shared edge twice and doubles up a see-through line.

### 10.18 `for` is inclusive on both ends

`for i = 0 to n` runs `n + 1` times (0..n). For `n` iterations use `for i = 0 to n - 1`.

## 11. Testing a shape in Editorial

Save the `.shape` file and its matching PNG in the `smartshapes` folder beside
`Editorial.exe`. Open an Image Block for editing, open the Auto Shapes picker and
select the new shape. If you edit a script that is already in the picker, save it and
select it again so Editorial reloads the changed script. Restart Editorial after
replacing an icon that is already displayed. Editorial displays a message with the
section and error when a script cannot be parsed, drawn or operated.

Before distributing a shape, test the complete customer workflow in Editorial:

1. Confirm that the picker shows the expected filename-derived name and icon.
2. Insert the shape with a click. It must draw at a useful default size without an
   error.
3. Drag to insert it. A box-style shape must fit the dragged box; a shape with
   `[insert]` must follow the intended from/to gesture and show a sensible live
   preview.
4. Double-click the shape to enter its handle-editing mode. Exercise every drag and
   click handle and confirm that its tooltip and behaviour agree.
5. Push every control to realistic extremes: minimum and maximum counts, very small
   and large radii, strong bends, and free points outside the original canvas. The
   artwork must not collapse, turn inside out or develop stray spikes.
6. Click every named part and test the appropriate Fill or Line controls; test line
   width on parts that draw a stroke. Each segment must select and recolour
   independently without changing an unrelated part.
7. Return to object mode, then move, resize and rotate the shape. Handles should edit
   only the intended parameters; whole-object transformations belong to Editorial.
8. Save and reopen the Editorial document, then export it. Confirm that geometry,
   text, colours, transparency and layering agree with the editing view.

Also review the script itself: every handle must have an exact matching `[drag:*]` or
`[click:*]` section; solid parts should have no outline; lines should have no fill; a
hole should wind opposite to its outer path; and `[draw]` must recompute exclusively
from parameters rather than locals left by another section.

Read the shipped samples in this folder as worked references: `star.shape` and
`smart_polygon.shape` (counts, radii, twist, slider handles), `talking.shape` and
`thinking.shape` (Bezier outlines, arc-length spacing, bend handles),
`arrow.shape` (endpoints, `[insert]`, endpoint-style click handles, per-part colour),
`measure_tool.shape` (`[insert]` line, text), `cog.shape` and `crescent.shape`
(circle-arc construction, holes), `clock.shape` and `calendar.shape` (dates, many
segments, grids), and `Box_3D.shape` (a three-dimensional shape: projection,
back-to-front drawing without a sort, four rail controls, one named part per
face).
