Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions Geometry documents/Animations.md
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,46 @@ if __name__ == '__main__':
| ~~**6b**~~ | `rt-viewmanager.js` | ~~`loadView()` with `skipCamera` option~~ | ~~Restores non-camera state only~~ | **DONE** `a106bf7` |
| ~~**6c**~~ | `rt-animate.js` | ~~`animateToViewFull()` + smooth cutplane interpolation~~ | ~~Bottom-row ▶ slerps camera + interpolates cutplane~~ | **DONE** `a106bf7` |
| ~~**6d**~~ | `index.html` | ~~HTML reorg: Move View Capture → "View Manager" section, rename Camera → View Manager~~ | ~~UI layout correct, all controls functional~~ | **DONE** |
| **7a** | `index.html`, `rt-viewmanager.js` | Re-Save button — overwrite active view's camera + scene state in-place | Navigate to view, adjust scene, click Re-Save, replay to verify | Pending |

## Phase 7: Re-Save View (In-Place Edit)

**Problem**: Once a view is saved, there's no way to adjust its camera or scene state without deleting and re-creating it — losing its position in the sequence, name, and transition duration.

**Solution**: A "Re-Save" button appears beside "Save" whenever a view is active (selected via ▶). Clicking it overwrites the active view's camera + scene state with the current scene, while preserving its identity.

### Workflow

1. Click ▶ on a view (e.g. PP-05) — camera animates there, PP-05 becomes active
2. User rotates camera, toggles forms, adjusts sliders — whatever changes they want
3. Click "Re-Save PP-05" — current state overwrites PP-05
4. Save button still works independently to create a new view

### What Gets Updated vs Preserved

| Updated (current state) | Preserved (view identity) |
|------------------------|--------------------------|
| Camera position/rotation/zoom | View ID |
| Cutplane state | View name |
| Render settings | Position in sequence |
| Instance references | Transition duration |
| Scene state delta | Label, sheet size |
| Axis code | Timestamp |

### Implementation

| Component | What |
|-----------|------|
| `index.html` | `#resaveViewBtn` beside Save, hidden by default (`display: none`) |
| `rt-viewmanager.js` | `resaveActiveView()` — captures fresh state, overwrites active view object |
| `rt-viewmanager.js` | `_updateResaveButton()` — shows/hides button, updates label to "Re-Save {name}" |
| `rt-viewmanager.js` | Button shown when `setActiveViewRow()` called, hidden on delete/clear |

### Delta Chain Integrity

Re-saving view N recomputes its delta relative to `getSnapshotAtView(N-1)`. Subsequent views' deltas (N+1, N+2, ...) remain untouched because they store absolute target values for changed fields. The cascade propagates naturally — if re-saving N changes a value that N+1 didn't touch, N+1 inherits the new value from N (correct behavior).

---

## Favicon-Specific Parameters

Expand Down
22 changes: 15 additions & 7 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3423,6 +3423,14 @@ <h3>
>
Save
</button>
<button
class="toggle-btn variant-standard"
id="resaveViewBtn"
title="Overwrite selected view with current state"
style="display: none"
>
Re-Save
</button>
</div>
</div>

Expand Down Expand Up @@ -3488,11 +3496,11 @@ <h3>
<div style="display: flex; gap: 4px; margin-top: 8px">
<button
class="toggle-btn variant-small"
id="downloadSelectedBtn"
title="Download selected view as SVG"
id="importViewsBtn"
title="Import views from file"
style="flex: 1"
>
SVG
Import
</button>
<button
class="toggle-btn variant-small"
Expand All @@ -3504,11 +3512,11 @@ <h3>
</button>
<button
class="toggle-btn variant-small"
id="importViewsBtn"
title="Import views from file"
id="downloadSelectedBtn"
title="Download selected view as SVG"
style="flex: 1"
>
Import
SVG
</button>
<button
class="toggle-btn variant-small"
Expand All @@ -3527,7 +3535,7 @@ <h3>

<!-- Animation Actions: Camera Only -->
<div style="margin-top: 8px">
<div style="font-size: 9px; color: #888; margin-bottom: 2px; text-transform: uppercase; letter-spacing: 0.5px">Camera</div>
<div style="font-size: 9px; color: #888; margin-bottom: 2px; text-transform: uppercase; letter-spacing: 0.5px">Camera Only</div>
<div style="display: flex; gap: 4px">
<button
class="toggle-btn variant-small"
Expand Down
117 changes: 117 additions & 0 deletions modules/rt-viewmanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ export const RTViewManager = {
saveViewBtn.addEventListener("click", () => this.saveCurrentView());
}

// Re-Save button (overwrites active view with current state)
const resaveViewBtn = document.getElementById("resaveViewBtn");
if (resaveViewBtn) {
resaveViewBtn.addEventListener("click", () => this.resaveActiveView());
}

// View name input - update placeholder on focus
const viewNameInput = document.getElementById("viewNameInput");
if (viewNameInput) {
Expand Down Expand Up @@ -1469,6 +1475,99 @@ ${rasterContent}${gridsContent}${facesContent}${edgesContent}${vectorContent}${n
console.log(`✅ View saved: ${view.name}`);
},

/**
* Overwrite the active view with the current camera + scene state.
* Preserves the view's identity (id, name, position, timing).
*/
resaveActiveView() {
if (!this.state.activeViewId) {
console.warn("No active view to re-save");
return;
}

const viewIndex = this.state.views.findIndex(
v => v.id === this.state.activeViewId
);
if (viewIndex < 0) {
console.warn("Active view not found in registry");
return;
}

const view = this.state.views[viewIndex];

// Compute accumulated snapshot up to (but not including) this view
const prevSnapshot =
viewIndex > 0 ? this.getSnapshotAtView(viewIndex - 1) : null;

// Capture current scene state and compute delta
const currentSnapshot = RTDelta.captureSnapshot();
const sceneState = RTDelta.computeDelta(prevSnapshot, currentSnapshot);

// Get current camera state
const cameraState = {
position: {
x: this._camera.position.x,
y: this._camera.position.y,
z: this._camera.position.z,
},
rotation: {
x: this._camera.rotation.x,
y: this._camera.rotation.y,
z: this._camera.rotation.z,
},
zoom: this._camera.zoom || 1,
type: this._camera.isPerspectiveCamera ? "perspective" : "orthographic",
};

// Get current cutplane state
const papercutState = this._papercut?.state || {};
const cutplaneState = {
enabled: papercutState.cutplaneEnabled || false,
axis: papercutState.cutplaneAxis || "z",
basis: papercutState.cutplaneBasis || "cartesian",
value: papercutState.cutplaneValue || 0,
inverted: papercutState.invertCutPlane || false,
};

// Get current render settings
const renderState = {
printMode: papercutState.printModeEnabled || false,
lineWeight: papercutState.lineWeightMax || 3,
sectionNodes: papercutState.sectionNodesEnabled || false,
adaptiveResolution: papercutState.adaptiveNodeResolution || false,
backfaceCulling: papercutState.backfaceCullingEnabled || true,
};

// Get current instance references
const instanceRefs = this._stateManager
? this._stateManager.state.instances.map(inst => inst.id)
: [];

// Update transient state (camera, scene, etc.)
view.camera = cameraState;
view.cutplane = cutplaneState;
view.render = renderState;
view.instanceRefs = instanceRefs;
view.sceneState = sceneState;
view.axisCode =
this._detectQuadrayAxis() || this._detectCameraAxis();

// Mark SVG as stale (needs re-export)
view.svg.exported = false;
view.svg.data = null;

// Re-render table and release focus
this.renderViewsTable();

const resaveBtn = document.getElementById("resaveViewBtn");
if (resaveBtn) resaveBtn.blur();
if (document.activeElement && document.activeElement !== document.body) {
document.activeElement.blur();
}

console.log(`🔄 View re-saved: ${view.name}`);
},

/**
* Download the currently selected/active view as SVG
*/
Expand Down Expand Up @@ -1659,6 +1758,7 @@ ${rasterContent}${gridsContent}${facesContent}${edgesContent}${vectorContent}${n
}

this.renderViewsTable();
this._updateResaveButton();
console.log(`🗑️ View deleted: ${view.name}`);
return true;
},
Expand All @@ -1684,6 +1784,7 @@ ${rasterContent}${gridsContent}${facesContent}${edgesContent}${vectorContent}${n
this.state.counters[key] = 0;
});
this.renderViewsTable();
this._updateResaveButton();
this._updateViewNamePlaceholder();
console.log("🗑️ All views cleared");
}
Expand Down Expand Up @@ -2056,6 +2157,22 @@ ${rasterContent}${gridsContent}${facesContent}${edgesContent}${vectorContent}${n
tbody.querySelectorAll(".view-row").forEach(row => {
row.classList.toggle("active", row.dataset.viewId === viewId);
});

this._updateResaveButton();
},

/**
* Show/hide the Re-Save button based on whether a view is active.
* @private
*/
_updateResaveButton() {
const btn = document.getElementById("resaveViewBtn");
if (!btn) return;

const hasActive = this.state.activeViewId &&
this.state.views.some(v => v.id === this.state.activeViewId);

btn.style.display = hasActive ? "" : "none";
},

// ========================================================================
Expand Down