# Editor API — driving the Stage Setter web editor

The editor lives at `https://itsmatchday.app/build/`. Drive it via `mcp__Claude_Preview__preview_eval` on a preview entry that opens that URL (typical name: `match-day-editor`).

## Starting the editor

```python
mcp__Claude_Preview__preview_start(name="match-day-editor")
# → returns serverId, port 8765

mcp__Claude_Preview__preview_eval(
  serverId=...,
  expression="window.location.href = '/build/index.html'; 'navigating'"
)
```

If a "Pick up where you left off?" restore dialog appears, dismiss it:

```javascript
const dlg = document.getElementById('restore-dialog');
if (dlg && dlg.style.display !== 'none') {
  const startFresh = Array.from(dlg.querySelectorAll('button')).find(b => /start fresh/i.test(b.textContent));
  if (startFresh) startFresh.click();
}
```

## Verifying the editor is ready

```javascript
return {
  hasApply: typeof applyStageJson,
  hasTo: typeof toStageJson,
  hasRender: typeof render,
  hasOpenBrief: typeof openBrief,
};
// expect all four to return 'function'
```

## Helper: set input value with proper events

The editor's state syncs from inputs via `input` + `change` events. Just setting `.value` isn't enough.

```javascript
function setVal(id, val) {
  const el = document.getElementById(id);
  if (!el) return false;
  el.value = val;
  el.dispatchEvent(new Event('input', { bubbles: true }));
  el.dispatchEvent(new Event('change', { bubbles: true }));
  return true;
}
```

## Stage Settings dialog (gear icon)

```javascript
document.getElementById('settings-gear-btn').click();
setVal('stage-code', 'FC-03');
setVal('stage-name', 'My Stage');
setVal('bay-width', '35');
setVal('bay-depth', '40');
setVal('stage-designer', 'Claude (Anthropic)');
Array.from(document.querySelectorAll('#stage-settings-dialog button'))
  .find(b => /done/i.test(b.textContent)).click();
```

## Stage Brief dialog

```javascript
openBrief();   // global function

setVal('brief-procedure', '...');
setVal('brief-start-condition', '...');
setVal('brief-stage-procedure', '...');
setVal('brief-scoring-type', 'comstock');  // or 'virginiaCount', 'fixedTime'
setVal('brief-leave-shooting-area', 'no');  // or 'yes' for multi-area
setVal('brief-round-count-override', '28');
setVal('brief-target-count-override', '14');
setVal('brief-points-mode', 'auto');  // or 'custom'
setVal('brief-scored-hits', 'Best 2 hits per paper');
setVal('brief-start-signal', 'Audible');
setVal('brief-end-condition', 'Last shot');
setVal('brief-gotchas', 'line 1\nline 2');
setVal('brief-penalties', 'Per current edition USPSA Handgun Competition Rules.');
setVal('brief-build-notes', '...long-form setup notes for the crew...');

Array.from(document.querySelectorAll('#brief-dialog button'))
  .find(b => /^save$/i.test(b.textContent.trim())).click();
```

## The applyStageJson round-trip pattern

The cleanest authoring path: take the current stage, mutate `props` / `markers` / etc., then re-apply.

```javascript
const uuid = () => crypto.randomUUID().toUpperCase();
const SHOULDER = 3.0316666666666663;  // standard 5-ft-at-shoulders mount height

function mount(type, hardcover=null) {
  return {
    type,                  // 'paperTarget' | 'noShoot'
    bottomHeightFeet: SHOULDER,
    facingOffsetDegrees: 0,
    label: null,
    hardcover,             // null | 'verticalLeft' | 'verticalRight' | 'horizontalTop' | 'horizontalBottom' | 'tuxedo' | 'diagonalUpperLeft' | 'diagonalUpperRight' | 'diagonalLowerLeft' | 'diagonalLowerRight'
    inverted: null,
  };
}

// Stand aligned along x (cardboard facing N or S — facing=0 or 180)
function uprangeStand(label, leftX, y) {
  return {
    kind: 'targetStand', id: uuid(), label,
    baseEndpointA: { x: leftX, y },
    baseEndpointB: { x: leftX + 1.5, y },
    facingDegrees: 180,
    mounts: [mount('paperTarget')]
  };
}

// Stand aligned along y (cardboard facing E or W — facing=90 or 270)
function sideStand(label, x, southY, facing) {
  return {
    kind: 'targetStand', id: uuid(), label,
    baseEndpointA: { x, y: southY },
    baseEndpointB: { x, y: southY + 1.5 },
    facingDegrees: facing,
    mounts: [mount('paperTarget')]
  };
}

// Angled NE (facing=135) — stand axis runs SE→NW. Cardboard faces NE.
function neAngledStand(label, cx, cy) {
  const half = 0.75 * Math.SQRT1_2;  // 0.5303
  return {
    kind: 'targetStand', id: uuid(), label,
    baseEndpointA: { x: cx + half, y: cy - half },
    baseEndpointB: { x: cx - half, y: cy + half },
    facingDegrees: 135,
    mounts: [mount('paperTarget')]
  };
}

// Angled NW (facing=225) — stand axis runs SW→NE. Cardboard faces NW.
function nwAngledStand(label, cx, cy) {
  const half = 0.75 * Math.SQRT1_2;
  return {
    kind: 'targetStand', id: uuid(), label,
    baseEndpointA: { x: cx - half, y: cy - half },
    baseEndpointB: { x: cx + half, y: cy + half },
    facingDegrees: 225,
    mounts: [mount('paperTarget')]
  };
}

// Wall (vertical 6-ft surface). Endpoints define the floor line.
function wall(label, x1, y1, x2, y2) {
  return {
    kind: 'wallSegment', id: uuid(), label,
    endpointA: { x: x1, y: y1 },
    endpointB: { x: x2, y: y2 },
    cutouts: []   // for ports/windows: [{ distanceAlongWallFeet, widthFeet, bottomHeightFeet, topHeightFeet, label }]
  };
}

// Fault line segment.
function fl(x1, y1, x2, y2, label) {
  return {
    kind: 'faultLineSegment', id: uuid(),
    label: label || null,
    endpointA: { x: x1, y: y1 },
    endpointB: { x: x2, y: y2 }
  };
}

// Start position — point + facing.
function start(x, y, facing) {
  return {
    kind: 'startPosition', id: uuid(),
    label: 'Start',
    position: { x, y },
    facingDegrees: facing   // 0=facing south/downrange
  };
}
```

## Applying a fresh stage

```javascript
const W = 35, D = 40;
const props = [ /* ... */ ];
const markers = [
  { id: 0, imageName: 'marker_0', physicalWidthMeters: 0.2032, position: { x: 0, y: 0 } },
  { id: 1, imageName: 'marker_1', physicalWidthMeters: 0.2032, position: { x: W, y: 0 } }
];
const current = toStageJson();
const merged = {
  ...current,
  schemaVersion: 2,
  id: crypto.randomUUID().toUpperCase(),
  code: 'FC-03',
  name: 'My Stage',
  sport: 'uspsa',
  designer: 'Claude (Anthropic)',
  bayWidthFeet: W,
  bayDepthFeet: D,
  barrelColor: 'white',
  markers,
  props,
  referenceMeasurements: [],
  brief: null
};
applyStageJson(merged);
if (typeof render === 'function') render();
```

## Incrementally adding props (don't rebuild from scratch)

```javascript
const stage = toStageJson();
stage.props.push( /* new prop */ );
applyStageJson(stage);
render();
```

## Removing props

```javascript
const stage = toStageJson();
stage.props = stage.props.filter(p => p.label !== 'T11' && p.label !== 'T16');
applyStageJson(stage);
render();
```

## Fitting the view + screenshotting

```javascript
const fitBtn = Array.from(document.querySelectorAll('button')).find(b => /^fit$/i.test(b.textContent.trim()));
if (fitBtn) fitBtn.click();
```

Then:
```python
mcp__Claude_Preview__preview_screenshot(serverId=...)
```

The bay aspect ratio (often tall + narrow) doesn't fit a single zoom. Strategies:
- Click `Fit` to auto-fit (usually correct)
- Two-shot top + bottom by scrolling `#canvas-scroll`'s scrollTop
- Or just show the JSON dump alongside a single screenshot

## Exporting the JSON

```javascript
const stage = toStageJson();
stage.author = 'Claude (Anthropic)';
if (!stage.roundCount || stage.roundCount === 12) stage.roundCount = 28; // or actual
return JSON.stringify(stage, null, 2);
```

Then write to wherever the user prefers (default suggestion: `~/Desktop/<CODE>_<Name>.json`) via the `Write` tool.

## Console check

```python
mcp__Claude_Preview__preview_console_logs(serverId=..., level="error", lines=30)
```

Errors after `applyStageJson` usually mean schema mismatch — props with wrong field names, missing `mounts` arrays, etc.
