diff --git a/web/i18n/ar.js b/web/i18n/ar.js
index f6b4f93..db85ecb 100644
--- a/web/i18n/ar.js
+++ b/web/i18n/ar.js
@@ -311,6 +311,8 @@ export default {
"Restore all": "استعادة الكل",
"Highlight small overhangs": "إبراز البروزات المعلّقة الصغيرة",
"Clear all": "مسح الكل",
+ "Remove model": "إزالة النموذج",
+ "Take this model off the plate, to import another without reloading the page.": "إزالة هذا النموذج من اللوحة لاستيراد نموذج آخر دون إعادة تحميل الصفحة.",
"Remove selected": "إزالة المحدد",
// Manual Draw hint
diff --git a/web/i18n/de.js b/web/i18n/de.js
index 54dff65..bca426a 100644
--- a/web/i18n/de.js
+++ b/web/i18n/de.js
@@ -324,6 +324,8 @@ export default {
"Restore all": "Alle wiederherstellen",
"Highlight small overhangs": "Kleine Überhänge hervorheben",
"Clear all": "Alle löschen",
+ "Remove model": "Modell entfernen",
+ "Take this model off the plate, to import another without reloading the page.": "Dieses Modell von der Platte nehmen, um ein anderes ohne Neuladen der Seite zu importieren.",
"Remove selected": "Ausgewählte entfernen",
// Manual Draw hint
diff --git a/web/i18n/nl.js b/web/i18n/nl.js
index fa5299d..3b86c0a 100644
--- a/web/i18n/nl.js
+++ b/web/i18n/nl.js
@@ -312,6 +312,8 @@ export default {
"Restore all": "Alles herstellen",
"Highlight small overhangs": "Kleine overhangen markeren",
"Clear all": "Alles wissen",
+ "Remove model": "Model verwijderen",
+ "Take this model off the plate, to import another without reloading the page.": "Haal dit model van de plaat om een ander te importeren zonder de pagina te herladen.",
"Remove selected": "Selectie verwijderen",
// Manual Draw hint
diff --git a/web/i18n/pt.js b/web/i18n/pt.js
index 1fd902d..1693f31 100644
--- a/web/i18n/pt.js
+++ b/web/i18n/pt.js
@@ -296,6 +296,8 @@ export default {
"Restore all": "Restaurar todas",
"Highlight small overhangs": "Destacar balanços pequenos",
"Clear all": "Limpar tudo",
+ "Remove model": "Remover modelo",
+ "Take this model off the plate, to import another without reloading the page.": "Tirar este modelo da mesa, para importar outro sem recarregar a página.",
"Remove selected": "Remover selecionado",
"Click two points across an overhang — straight onto the red faces — to lay a breakaway wall along that line. Click an upright side once to stand a sway brace against it. Esc or right-click cancels.": "Clique em dois pontos atravessando um balanço — direto nas faces vermelhas — para criar uma parede destacável ao longo dessa linha. Clique uma vez num lado vertical para apoiar uma escora lateral nele. Esc ou botão direito cancela.",
"off": "desligado",
diff --git a/web/i18n/zh.js b/web/i18n/zh.js
index e51aee6..2b10bb6 100644
--- a/web/i18n/zh.js
+++ b/web/i18n/zh.js
@@ -327,6 +327,8 @@ export default {
"Restore all": "全部恢复",
"Highlight small overhangs": "高亮细小悬垂",
"Clear all": "全部清除",
+ "Remove model": "移除模型",
+ "Take this model off the plate, to import another without reloading the page.": "将此模型从打印板上移除,无需重新加载页面即可导入另一个模型。",
"Remove selected": "移除所选",
// Manual Draw hint
diff --git a/web/index.html b/web/index.html
index 9be7f50..972e8ad 100644
--- a/web/index.html
+++ b/web/index.html
@@ -204,6 +204,8 @@
Bed pad
—
Analysis
—
+
diff --git a/web/style.css b/web/style.css
index b0957df..dcb697a 100644
--- a/web/style.css
+++ b/web/style.css
@@ -347,6 +347,7 @@ html, body {
fin/pad time is appended, and that one-line growth used to nudge everything below
the panel down when fins were added. Always two lines high = no shift. */
#stats #s-time { display: block; min-height: 2.6em; line-height: 1.3; }
+#stats #remove-part { margin-top: 10px; }
#stats hr {
border: 0;
border-top: 1px solid var(--line);
diff --git a/web/ui/io.js b/web/ui/io.js
index 89e1e11..1e87abf 100644
--- a/web/ui/io.js
+++ b/web/ui/io.js
@@ -63,6 +63,14 @@ export function renderImportNote() {
/** The part was rescaled (ui/scale.js): the supports a saved session brought back
* are gone with it, so the note no longer says they were restored. */
+/** The part was taken off the plate (part.js clearPart): its import note goes too. */
+export function forgetImport() {
+ importNote = '';
+ lastImportMeta = null;
+ pendingSession = null;
+ el('s-import-note').textContent = '';
+}
+
export function forgetSessionNote() {
if (!lastImportMeta?.session) return;
lastImportMeta.session = null;
diff --git a/web/ui/part.js b/web/ui/part.js
index a20f77f..91c1c47 100644
--- a/web/ui/part.js
+++ b/web/ui/part.js
@@ -7,14 +7,14 @@ import { t, tn } from './i18n.js';
import * as THREE from 'three';
import { buildTopology, analyze, DEFAULT_THRESHOLD, MIN_REGION_AREA } from '../overhangs.js';
import { el } from './dom.js';
-import { scene, controls, frame, clip, MODEL } from './scene.js';
+import { scene, controls, frame, clip, MODEL, requestRender } from './scene.js';
import { setSection, followPart } from './section.js';
import { showToolbar } from './toolbar.js';
import { removeMode, cancelRemove, resetRemovals } from './remove.js';
import { resetHistory } from './history.js';
-import { importNote, renderImportNote } from './io.js';
+import { importNote, renderImportNote, forgetImport } from './io.js';
import { currentVolume } from './volume.js';
-import { resetLoad, updateLayerView, updateLoadReadout, syncLoadUI } from './strength.js';
+import { resetLoad, updateLayerView, hideLayerView, updateLoadReadout, syncLoadUI } from './strength.js';
import { hideSuggestions } from './suggest.js';
import {
setDrawnWalls, setDrawMsg, markPrintTrisDirty, clearPreview, syncDrawControls,
@@ -109,7 +109,21 @@ export function setPart(geometry, filename, { quat } = {}) {
gizmo.attach(part);
showToolbar(true);
- // A new part starts with no hand-drawn walls and a fresh print-space cache.
+ resetPartState();
+
+ const size = shade();
+ frame(size);
+ resetScale();
+ return size;
+}
+
+/**
+ * What a part's tools hold that means nothing on another part (or on none): the
+ * drawn walls, removed fins, armed modes, load arrow, Suggest list, section cut and
+ * undo history. Shared by setPart and clearPart.
+ */
+function resetPartState() {
+ // A new part (or none) starts with no hand-drawn walls and a fresh print-space cache.
setDrawnWalls([]);
// Per-fin removals are keyed by a content signature that can coincidentally
// match a different model's fins, so they must NOT carry across parts -- clear
@@ -124,7 +138,7 @@ export function setPart(geometry, filename, { quat } = {}) {
clearPreview();
syncAugmentUI(); // "+ Add" is off now: the button and the Draw hint say so
syncDrawControls();
- // A new part starts with no load direction either, and no Suggest ranking: the
+ // A new part (or none) starts with no load direction either, and no Suggest ranking: the
// list and verdict were the OLD part's poses.
resetLoad();
hideSuggestions();
@@ -136,11 +150,37 @@ export function setPart(geometry, filename, { quat } = {}) {
// Undo history does not carry across parts.
resetHistory();
+}
- const size = shade();
- frame(size);
+/**
+ * Take the part off the plate (the stats card's Remove model): back to the empty
+ * page a fresh tab shows, so the next model needs no reload. The fin settings stay,
+ * as they do across a load.
+ */
+export function clearPart() {
+ if (!part) return;
+ gizmo.detach();
+ part.remove(hoverFace);
+ hoverFace.visible = false;
+ scene.remove(part);
+ part.geometry.dispose();
+ part = null;
+ topology = null;
+ lastResult = lastShadeRes = null;
+ flatRegions = null;
+ painted = paintedAttr = null;
+ lastSize = null;
+ partName = '';
+ resetPartState();
+ refreshFins(); // no lastResult now: drops the fins, pad and readout
resetScale();
- return size;
+ forgetImport();
+ hideLayerView();
+ showToolbar(false);
+ el('stats').hidden = true;
+ el('status').hidden = true;
+ el('drop').classList.remove('hidden');
+ requestRender();
}
/**
@@ -431,4 +471,6 @@ export function syncPartLanguage() {
renderIslandText();
}
+el('remove-part').addEventListener('click', clearPart);
+
window.addEventListener('languagechange', syncPartLanguage);
diff --git a/web/ui/strength.js b/web/ui/strength.js
index de77c47..632d195 100644
--- a/web/ui/strength.js
+++ b/web/ui/strength.js
@@ -108,6 +108,11 @@ export function updateLayerView(size) {
layerViz.visible = layersOn && !!part;
}
+/** No part on the plate: no layer frames either. */
+export function hideLayerView() {
+ layerViz.visible = false;
+}
+
// ---------------------------------------------------------------- load arrow UI
/**
diff --git a/web/ui/toolbar.js b/web/ui/toolbar.js
index 9e5be87..d499995 100644
--- a/web/ui/toolbar.js
+++ b/web/ui/toolbar.js
@@ -18,8 +18,8 @@
* 3D view leaves it open (Lay a face flat and Section are used there); Esc folds it
* and hands focus back to its badge. `onFold` runs when a popover folds, for a mode
* armed from inside it that must not stay armed out of sight (Lay a face flat).
- * The bar shows once a part is loaded (showToolbar, from part.js); nothing unloads
- * a part, so it is never hidden again.
+ * The bar shows while a part is loaded (showToolbar, from part.js) and hides again
+ * when Remove model takes it off, folding any open popover.
*
* It sits bottom-left, at the window's edge -- or just right of the stats card when
* a short window has scrolled the card down beside it.
@@ -112,5 +112,6 @@ addEventListener('keydown', (e) => {
/** Show the bar (a part is loaded) or hide it. */
export function showToolbar(v) {
el('viewbar').hidden = !v;
+ if (!v && openId) { openId = null; render(); }
place();
}