# Incremental Subpage Dimensions — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Replace absolute-position tick-mark dimensions on portal subpages with incremental chain dimensions using closed arrow heads at both ends, starting from 0.

**Architecture:** New JS function `computeIncrementalDimCoords` reads sorted positions from `above_dimensions`/`below_dimensions`, adds 0 as first position, and for each consecutive pair precomputes: horizontal line, left/right arrow polygons (as triangle point arrays), centered label position, and delta label text. All in profile inches. JS renderer and PDF both read this precomputed data — no recalculation.

**Tech Stack:** Phaser 3 (JS), Ruby/Prawn (PDF), existing arrow infrastructure from plans

---

## File Map

| File | Action | Responsibility |
|---|---|---|
| `dialogs/phaser/paper_view/panels/paper_view_portal_subpage.js` | Rewrite | New `computeIncrementalDimCoords`, rewrite `drawSubpageDimensions`, remove old `computeSubpageDimCoords` + `addEmptyDimLines` |
| `dialogs/phaser/paper_view/paper_view_export_panels.js` | Modify | Call new function from export path; add standalone copy |
| `manage_2D/paper_view/panels/create_panels_pdf.rb` | Rewrite | `draw_subpage_dimensions_pdf` reads precomputed incremental data |
| `manage_2D/paper_view/panels/panel_paper_view_subpages.rb` | No change | Position arrays already generated correctly |

---

### Task 1: Add `computeIncrementalDimCoords` standalone function (JS export)

**Files:**
- Modify: `dialogs/phaser/paper_view/paper_view_export_panels.js`

- [ ] **Step 1: Add function before `ensureWebHoleSideData`**

Insert after `computeBrokenBorderLines` and before `ensureWebHoleSideData`:

```js
// Compute incremental dimension chains for subpage hole/cut positions.
// Replaces the old absolute-position tick-mark style.  Stores precomputed
// lines, arrow polygons, label positions, and delta labels on each dim
// entry so the PDF can draw them without recalculation.
// All coordinates in profile inches (y=0 at top of web).
function computeIncrementalDimCoords(prof, fontSize, side) {
  let dims = side === 'above' ? prof.above_dimensions : prof.below_dimensions
  if (!dims || dims.length === 0) return

  // Already computed (memoized by checking for incremental flag)
  if (dims._incremental_computed) return

  let isAbove = side === 'above'
  let sectionH = prof.section_height
  let dimGapIn = fontSize * 2.5  // inches from profile edge to dimension line
  let dimYIn = isAbove ? -dimGapIn : sectionH + dimGapIn

  // Arrow geometry in inches (proportional to font size for readability)
  let arrowLenIn = fontSize * 0.18   // arrow length
  let arrowHalfW = arrowLenIn / 4    // half base width

  // Collect sorted unique positions, including 0 at start
  let positions = dims.map(d => d.position).sort((a, b) => a - b)
  positions = [0.0].concat(positions.filter((p, i, arr) => i === 0 || p !== arr[i - 1]))

  // Build incremental entries: each consecutive pair becomes one dim
  let incremental = []
  for (let i = 0; i < positions.length - 1; i++) {
    let prev = positions[i]
    let curr = positions[i + 1]
    let delta = curr - prev
    if (delta < 0.001) continue

    let midX = (prev + curr) / 2

    // Horizontal line from prev to curr
    let lines = [[prev, dimYIn, curr, dimYIn]]

    // Left arrow (at prev, pointing right → toward +x)
    let arrowsLeft = [
      [prev, dimYIn],                                    // tip
      [prev + arrowLenIn, dimYIn - arrowHalfW],          // base top
      [prev + arrowLenIn, dimYIn + arrowHalfW],          // base bottom
    ]

    // Right arrow (at curr, pointing left → toward -x)
    let arrowsRight = [
      [curr, dimYIn],                                    // tip
      [curr - arrowLenIn, dimYIn - arrowHalfW],          // base top
      [curr - arrowLenIn, dimYIn + arrowHalfW],          // base bottom
    ]

    incremental.push({
      position: curr,
      lines: lines,
      arrows_left: arrowsLeft,
      arrows_right: arrowsRight,
      text_x: midX,
      text_y: dimYIn,
      label: '',  // filled by renderer below via getTextUnitFromInch
      delta_inch: delta,
    })
  }

  // Replace dims array content with incremental entries
  dims.length = 0
  incremental.forEach(d => dims.push(d))
  dims._incremental_computed = true
}
```

---

### Task 2: Update `ensureSubpageDimensions` to call new function

**Files:**
- Modify: `dialogs/phaser/paper_view/paper_view_export_panels.js`

- [ ] **Step 1: In the profile loop, add call BEFORE early return**

Replace the existing unconditional `computeBrokenBorderLines` block with an expanded block that also calls `computeIncrementalDimCoords`. The new block goes right before the early-return guard:

```js
      profiles.forEach((prof, i) => {
        // Always compute broken-segment border lines + incremental dims
        // for PDF — must run before the early-return guard below.
        if (prof.is_broken) {
          (prof.segments || []).forEach(seg => {
            computeBrokenBorderLines(seg, prof)
            computeIncrementalDimCoords(seg, fontSize, 'above')
            computeIncrementalDimCoords(seg, fontSize, 'below')
          })
        }
        computeIncrementalDimCoords(prof, fontSize, 'above')
        computeIncrementalDimCoords(prof, fontSize, 'below')

        let aboveDims = prof.above_dimensions || []
        if (aboveDims.length > 0 && aboveDims[0].lines) return
```

---

### Task 3: Update `ensureBrokenSubpageDimensions` to call new function

**Files:**
- Modify: `dialogs/phaser/paper_view/paper_view_export_panels.js`

- [ ] **Step 1: Add incremental dim calls in the segments loop**

```js
  segments.forEach(seg => {
    computeIncrementalDimCoords(seg, fontSize, 'above')
    computeIncrementalDimCoords(seg, fontSize, 'below')
    computeBrokenBorderLines(seg, prof)
  })
```

Remove the old `computeSubpageDimCoordsViaScene` calls since incremental dims don't need them:

```js
  segments.forEach(seg => {
    computeIncrementalDimCoords(seg, fontSize, 'above')
    computeIncrementalDimCoords(seg, fontSize, 'below')
    computeBrokenBorderLines(seg, prof)
  })
```

---

### Task 4: Rewrite `drawSubpageDimensions` in portal subpage JS

**Files:**
- Modify: `dialogs/phaser/paper_view/panels/paper_view_portal_subpage.js`
  - Rewrite `drawSubpageDimensions` method (lines ~127-199)
  - Remove `computeSubpageDimCoords` method (lines ~207-289)
  - Remove `addEmptyDimLines` method (lines ~291-319)
  - Remove call to `this.computeSubpageDimCoords` inside `drawSubpageDimensions` (line ~135)

- [ ] **Step 1: Add incremental dim computation call at top of `drawSubpageDimensions`**

Replace the `if (profileData) { this.computeSubpageDimCoords(...) }` call with:

```js
  drawSubpageDimensions(dims, x0, y0, rect_w, rect_h, scale, side, flangeH = 0, profileData = null) {
    if (!dims || dims.length === 0) return

    // Compute incremental dims if not already done (on-screen first pass)
    if (profileData) {
      let fontSize = (this.subpages_dimensions_font_size || 8)
      computeIncrementalDimCoords(profileData, fontSize, side)
    }
```

- [ ] **Step 2: Rewrite the rendering loop**

Replace the entire body after the computation block (old lines ~137-199) with:

```js
    let fontSize = (this.subpages_dimensions_font_size || 8)

    let dimGfx = this.add.graphics({
      lineStyle: { width: this.subpages_dimensions_thickness * 0.6, color: this.subpages_dimensions_color },
      fillStyle: { color: this.subpages_dimensions_color }
    })
    dimGfx.destroyOnReset = true
    dimGfx.depth = TEXT_DEPTH

    let arrowLen = fontSize * 0.18 * scale
    let arrowHalfW = arrowLen / 4

    dims.forEach(dim => {
      // Draw horizontal line
      if (dim.lines) {
        dim.lines.forEach(([x1, y1, x2, y2]) => {
          dimGfx.beginPath()
          dimGfx.moveTo(x0 + x1 * scale, y0 + y1 * scale)
          dimGfx.lineTo(x0 + x2 * scale, y0 + y2 * scale)
          dimGfx.strokePath()
        })
      }

      // Draw left arrow (filled triangle)
      if (dim.arrows_left) {
        let pts = dim.arrows_left.map(([ax, ay]) => ({
          x: x0 + ax * scale,
          y: y0 + ay * scale
        }))
        dimGfx.fillTriangle(pts[0].x, pts[0].y, pts[1].x, pts[1].y, pts[2].x, pts[2].y)
      }

      // Draw right arrow (filled triangle)
      if (dim.arrows_right) {
        let pts = dim.arrows_right.map(([ax, ay]) => ({
          x: x0 + ax * scale,
          y: y0 + ay * scale
        }))
        dimGfx.fillTriangle(pts[0].x, pts[0].y, pts[1].x, pts[1].y, pts[2].x, pts[2].y)
      }

      // Draw label (centered, no rotation)
      let labelStr = dim.delta_inch !== undefined
        ? this.getTextUnitFromInch(dim.delta_inch)
        : ''
      dim.label = labelStr

      let label = this.add.text(
        x0 + dim.text_x * scale,
        y0 + dim.text_y * scale,
        labelStr,
        {
          fontSize: fontSize,
          fontFamily: this.font_family,
          color: getHexStringFromInt(this.subpages_dimensions_color),
          resolution: this.font_resolution,
        }
      )
      label.setOrigin(0.5, 0.5)
      label.destroyOnReset = true
      label.depth = TEXT_DEPTH
    })
  }
```

- [ ] **Step 3: Remove `computeSubpageDimCoords` method**

Delete the entire method (old lines ~204-289, from `// Shared dimension-coordinate computation...` comment through the closing `}`)

- [ ] **Step 4: Remove `addEmptyDimLines` method**

Delete the entire method (old lines ~291-319, `addEmptyDimLines(...)` through its closing `}`)

---

### Task 5: Rewrite PDF `draw_subpage_dimensions_pdf`

**Files:**
- Modify: `manage_2D/paper_view/panels/create_panels_pdf.rb`
  - Rewrite `draw_subpage_dimensions_pdf` (lines ~530-573)

- [ ] **Step 1: Replace method with incremental-aware version**

```ruby
  def draw_subpage_dimensions_pdf(dims, rect_x, rect_y_top, scale)
    return if dims.nil? || dims.empty?

    dim_font_size = (@subpages_dimensions_font_size || 8)
    stroke_prawn_color(@subpages_dimensions_color)
    fill_prawn_color(@subpages_dimensions_color)
    self.line_width = @subpages_dimensions_thickness * 0.6

    dims.each do |dim|
      # Horizontal line
      js_lines = dim[:lines]
      if js_lines.is_a?(Array) && js_lines.any?
        js_lines.each do |x1, y1, x2, y2|
          stroke_line(
            [rect_x + x1.to_f * 72 * scale, rect_y_top - y1.to_f * 72 * scale],
            [rect_x + x2.to_f * 72 * scale, rect_y_top - y2.to_f * 72 * scale]
          )
        end
      end

      # Left arrow (filled triangle)
      arrows_left = dim[:arrows_left]
      if arrows_left.is_a?(Array) && arrows_left.length == 3
        pts = arrows_left.map do |ax, ay|
          [rect_x + ax.to_f * 72 * scale, rect_y_top - ay.to_f * 72 * scale]
        end
        fill_polygon(pts[0], pts[1], pts[2])
      end

      # Right arrow (filled triangle)
      arrows_right = dim[:arrows_right]
      if arrows_right.is_a?(Array) && arrows_right.length == 3
        pts = arrows_right.map do |ax, ay|
          [rect_x + ax.to_f * 72 * scale, rect_y_top - ay.to_f * 72 * scale]
        end
        fill_polygon(pts[0], pts[1], pts[2])
      end

      # Label (centered, no rotation)
      dim_label = dim[:label] || ""
      text_x = dim[:text_x]
      text_y = dim[:text_y]
      next if text_x.nil? || text_y.nil? || dim_label.empty?

      fill_prawn_color(@subpages_dimensions_color)
      font(@font_name) do
        text_width = width_of(dim_label, size: dim_font_size.to_i)
        px = rect_x + text_x.to_f * 72 * scale
        py = rect_y_top - text_y.to_f * 72 * scale
        draw_text dim_label,
                  at: [px - text_width / 2.0, py + dim_font_size / 3.0],
                  size: dim_font_size.to_i
      end
      stroke_prawn_color(@subpages_dimensions_color)
    end
  rescue => e
    puts "draw_subpage_dimensions_pdf error: #{e.message}"
  end
```

---

### Task 6: Clean up `drawSubpageProfile` — remove old computeSubpageDimCoords calls

**Files:**
- Modify: `dialogs/phaser/paper_view/panels/paper_view_portal_subpage.js`

- [ ] **Step 1: Update `drawSubpageProfile` dimension calls**

The non-broken profile path calls `this.drawSubpageDimensions(...)` with `profileData` — the incremental computation happens inside `drawSubpageDimensions` now. No change needed to the call itself, but verify the `computeIncrementalDimCoords` is available (it's a global from export_panels.js).

The call at line ~97: `this.drawSubpageDimensions(profile_data.above_dimensions, x0, y0, rect_w, rect_h, scale, 'above', flange_h, profile_data)` — this is fine, `drawSubpageDimensions` now calls `computeIncrementalDimCoords(profileData, fontSize, side)` internally.

---

## Self-Review

1. **Spec coverage:**
   - [x] Incremental chains from 0 → Task 1 computes positions starting at 0
   - [x] Arrow heads at both ends → Tasks 1, 4, 5
   - [x] Labels between ticks, no rotation → Tasks 4, 5
   - [x] Broken segments independent → Tasks 2, 3 apply per-segment
   - [x] PDF read-only → All precomputed in JS, PDF only reads
   - [x] Reuse existing arrow infrastructure → Arrow polygons precomputed as triangles; same shape as existing closed arrows

2. **Placeholder scan:** None found.

3. **Type consistency:**
   - `computeIncrementalDimCoords(prof, fontSize, side)` — signature consistent across Tasks 1, 2, 3
   - `dim.arrows_left`, `dim.arrows_right`, `dim.lines`, `dim.text_x`, `dim.text_y`, `dim.label`, `dim.delta_inch` — consistent across JS and Ruby
   - `dims._incremental_computed` — memoization flag, internal only
