Compact STL list and clarify auto iteration

This commit is contained in:
Codex
2026-05-31 01:27:32 +08:00
parent 5cac15fbed
commit e2be49677f
3 changed files with 112 additions and 48 deletions

View File

@@ -900,6 +900,11 @@ function stlPresentation(file) {
};
}
function compactStlName(file) {
return String(file.segment_name || file.family || file.file_name || `STL_${file.id || ""}`)
.replace(/\.stl$/i, "");
}
function stlFileIndex(file) {
const id = normalizedStlId(file);
const index = state.stlFiles.findIndex((item) => normalizedStlId(item) === id);
@@ -976,6 +981,37 @@ async function setAllStlMode(mode) {
await applyStlSelectionChange();
}
function aggregateStlMode() {
if (!state.stlFiles.length || !state.selectedStlIds.size) return "hidden";
const modes = state.stlFiles.map((file) => stlMode(file));
if (modes.every((mode) => mode === "solid")) return "solid";
if (modes.every((mode) => mode === "translucent")) return "translucent";
if (modes.every((mode) => mode === "hidden")) return "hidden";
return "mixed";
}
function renderStlVisibilityAction() {
const button = $("cycleAllStlModeBtn");
if (!button) return;
const mode = aggregateStlMode();
button.classList.remove("hidden-eye", "solid-eye", "translucent-eye", "mixed-eye");
button.classList.add(`${mode === "mixed" ? "mixed" : mode}-eye`);
const label = {
hidden: "当前全部隐藏,点击切换为全部实体",
solid: "当前全部实体,点击切换为全部半透明",
translucent: "当前全部半透明,点击切换为全部隐藏",
mixed: "当前为混合显示,点击切换为全部实体",
}[mode];
button.title = label;
button.setAttribute("aria-label", label);
}
async function cycleAllStlMode() {
const mode = aggregateStlMode();
const next = mode === "solid" ? "translucent" : mode === "translucent" ? "hidden" : "solid";
await setAllStlMode(next);
}
function currentSeries() {
return state.series.find((item) => item.series_uid === state.selectedSeriesUid) || null;
}
@@ -1078,6 +1114,7 @@ async function selectSeries(uid, options = {}) {
function renderStl() {
$("stlCount").textContent = state.stlFiles.length ? `${state.selectedStlIds.size}/${state.stlFiles.length}` : "0";
renderStlVisibilityAction();
const models = [...new Set(state.stlFiles.map((item) => item.algorithm_model || state.algorithmModel || "未指定模型"))];
$("modelRail").innerHTML = models.map((model) => `<span class="chip active">${escapeHtml(model)}</span>`).join("");
if (!state.stlFiles.length) {
@@ -1124,9 +1161,7 @@ function renderStl() {
return `
<button class="stl-row stl-mode-${mode} ${mode !== "hidden" ? "active" : ""}" type="button" data-stl-mode="${id}" style="--stl-color:${escapeHtml(color)}" title="${escapeHtml(file.file_name)}">
<span class="stl-row-main">
<strong>${escapeHtml(presentation.itemLabel || file.segment_name || file.file_name)}</strong>
<small>${escapeHtml(file.family || "未分类")} · ${escapeHtml(file.category || "")} · ${formatBytes(file.size_bytes)}</small>
<small class="stl-file-name">${escapeHtml(file.file_name)}</small>
<strong>${escapeHtml(compactStlName(file))}</strong>
</span>
<span class="stl-visibility-swatch ${mode}" aria-label="${escapeHtml(STL_DISPLAY_LABELS[mode])}"></span>
</button>
@@ -3122,6 +3157,14 @@ function candidateScore(pose, settings = null) {
return geometryScore;
}
function describeAutoReference(settings) {
const records = autoReferenceRecords(settings);
if (!records.length) return "当前没有可用参考 STL";
const names = records.slice(0, 4).map((record) => compactStlName(record.file));
const suffix = records.length > names.length ? `${records.length}` : "";
return `参考 STL${names.join("、")}${suffix}`;
}
function estimateCoarsePose(settings) {
const records = autoReferenceRecords(settings);
const modelBox = visibleModelBox(records);
@@ -3219,6 +3262,7 @@ async function runAutoFine(options = {}) {
let best = { pose: { ...state.pose }, score: candidateScore(state.pose, settings), mode: "初始位姿" };
let evaluated = 1;
const coarsePose = estimateCoarsePose(settings);
const coarseSeed = coarsePose ? { pose: coarsePose, mode: "粗配准种子" } : null;
if (coarsePose) {
const coarseScore = candidateScore(coarsePose, settings);
evaluated += 1;
@@ -3241,9 +3285,19 @@ async function runAutoFine(options = {}) {
const nextValue = key === "scale" ? Math.max(0.2, Math.min(3, baseValue + delta)) : baseValue + delta;
return { ...sourcePose, [key]: Number(nextValue.toFixed(key === "scale" ? 3 : 4)) };
};
const searchBases = [{ pose: best.pose, mode: best.mode }];
if (coarseSeed && poseSignature(coarseSeed.pose) !== poseSignature(best.pose)) searchBases.push(coarseSeed);
const seenCandidates = new Set();
const addCandidate = (pose, mode) => {
const signature = poseSignature(pose);
if (seenCandidates.has(signature)) return;
seenCandidates.add(signature);
candidates.push({ pose, mode });
};
searchBases.forEach((base) => {
enabledKeys.forEach((key) => {
candidates.push({ pose: poseWithDelta(best.pose, key, stepForKey(key)), mode: `${key} +` });
candidates.push({ pose: poseWithDelta(best.pose, key, -stepForKey(key)), mode: `${key} -` });
addCandidate(poseWithDelta(base.pose, key, stepForKey(key)), `${base.mode} · ${key} +`);
addCandidate(poseWithDelta(base.pose, key, -stepForKey(key)), `${base.mode} · ${key} -`);
});
for (let left = 0; left < enabledKeys.length; left += 1) {
for (let right = left + 1; right < enabledKeys.length; right += 1) {
@@ -3252,16 +3306,19 @@ async function runAutoFine(options = {}) {
const leftKey = enabledKeys[left];
const rightKey = enabledKeys[right];
const pose = poseWithDelta(
poseWithDelta(best.pose, leftKey, stepForKey(leftKey, 0.65) * leftSign),
poseWithDelta(base.pose, leftKey, stepForKey(leftKey, 0.65) * leftSign),
rightKey,
stepForKey(rightKey, 0.65) * rightSign,
);
candidates.push({ pose, mode: `${leftKey}/${rightKey} 联合` });
addCandidate(pose, `${base.mode} · ${leftKey}/${rightKey} 联合`);
});
});
}
}
for (const candidate of candidates.slice(0, settings.candidates)) {
});
const candidateLimit = Math.max(settings.candidates, settings.candidates * searchBases.length);
const candidatesToEvaluate = candidates.slice(0, candidateLimit);
for (const candidate of candidatesToEvaluate) {
const pose = candidate.pose;
const score = candidateScore(pose, settings);
evaluated += 1;
@@ -3269,7 +3326,7 @@ async function runAutoFine(options = {}) {
}
const progress = 40 + ((index + 1) / Math.max(rounds.length, 1)) * 48;
const scoreText = Number.isFinite(best.score) ? best.score.toFixed(4) : "不可用";
setAutoProgress(true, progress, `候选评分 ${index + 1}/${rounds.length}`, `本轮评估 ${candidates.length} 个候选,累计 ${evaluated} 个;当前最 ${best.mode}score ${scoreText}`, "score");
setAutoProgress(true, progress, `候选评分 ${index + 1}/${rounds.length}`, `本轮评估 ${candidatesToEvaluate.length}/${candidates.length} 个候选,累计 ${evaluated} 个;当前最高分候选 ${best.mode}score ${scoreText}`, "score");
await waitForUiFrame();
}
setAutoProgress(true, 94, "应用最佳位姿", "正在把本轮最高分候选写回位姿控件和融合视图。", "apply");
@@ -3283,11 +3340,12 @@ async function runAutoFine(options = {}) {
const changed = poseSignature(best.pose) !== poseSignature(before);
setAutoState(changed ? "微调完成" : "微调无变化", changed ? "ok" : "warn");
const scoreText = Number.isFinite(best.score) ? best.score.toFixed(4) : "不可用";
const referenceText = describeAutoReference(settings);
$("autoResult").textContent = changed
? `最佳候选:${best.mode}score ${scoreText};评估 ${evaluated} 个候选;${settings.imageContext ? "骨窗像素采样 + STL 参考评分" : "参考 STL 盒体评分"}`
: `已完成评估但最佳仍为初始位姿score ${scoreText};评估 ${evaluated} 个候选。说明本轮候选没有比当前位姿更高分;若图像上仍错位,通常需要先调整大角度旋转或粗配准后再微调`;
? `本轮最高分候选:${best.mode}score ${scoreText};评估 ${evaluated} 个候选;${settings.imageContext ? "骨窗像素采样 + STL 参考评分" : "参考 STL 盒体评分"}${referenceText}`
: `本轮未找到比当前位姿更高分的候选score ${scoreText};评估 ${evaluated} 个候选。这不代表已配准正确,只说明当前搜索范围和参考 STL 下没有更高分;${referenceText};自动微调目前只调整平移/缩放,明显旋转错位需要先手动校正`;
updateAutoPoseResult(before, state.pose);
setAutoProgress(true, 100, changed ? "微调完成" : "未找到更优候选", changed ? `已应用 ${best.mode}` : "候选均未超过当前位姿,因此没有强行移动模型。", "apply");
setAutoProgress(true, 100, changed ? "微调完成" : "本轮未改动", changed ? `已应用 ${best.mode}` : "没有强行移动模型;请检查参考 STL 和大角度旋转。", "apply");
markDirty();
} catch (error) {
setAutoState("微调失败", "error");
@@ -3480,9 +3538,7 @@ function wireEvents() {
$("saveBtn").addEventListener("click", () => saveRegistration());
$("statusBtn").addEventListener("click", () => saveRegistration(state.registrationStatus === "registered" ? "unregistered" : "registered"));
$("notes").addEventListener("input", () => markDirty("notes"));
$("hideAllStlBtn").addEventListener("click", () => setAllStlMode("hidden"));
$("solidAllStlBtn").addEventListener("click", () => setAllStlMode("solid"));
$("translucentAllStlBtn").addEventListener("click", () => setAllStlMode("translucent"));
$("cycleAllStlModeBtn").addEventListener("click", cycleAllStlMode);
$("resetRotationBtn").addEventListener("click", () => resetPose("rotation"));
$("resetTransformBtn").addEventListener("click", () => resetPose("transform"));
$("resetFlipBtn").addEventListener("click", () => resetPose("flip"));

View File

@@ -170,9 +170,7 @@
<div class="tool-section-title compact-title">
<span>STL</span>
<div class="title-actions model-visibility-actions">
<button id="hideAllStlBtn" class="model-eye-btn hidden-eye" type="button" title="全部取消显示" aria-label="全部取消显示"><span></span></button>
<button id="solidAllStlBtn" class="model-eye-btn solid-eye" type="button" title="全部实显" aria-label="全部实显"><span></span></button>
<button id="translucentAllStlBtn" class="model-eye-btn translucent-eye" type="button" title="全部半透明" aria-label="全部半透明"><span></span></button>
<button id="cycleAllStlModeBtn" class="model-eye-btn" type="button" title="切换全部 STL 显示状态" aria-label="切换全部 STL 显示状态"><span></span></button>
</div>
<em id="stlCount">0</em>
</div>

View File

@@ -817,7 +817,7 @@ button {
}
.tool-section-title button.model-eye-btn {
width: 30px;
width: 34px;
height: 28px;
display: inline-flex;
align-items: center;
@@ -861,6 +861,15 @@ button {
rgba(45, 212, 191, 0.22);
}
.mixed-eye span::before {
border-color: #93c5fd;
background: linear-gradient(90deg, rgba(45, 212, 191, 0.26) 0 50%, rgba(15, 23, 42, 0.74) 50% 100%);
}
.mixed-eye span::after {
background: #93c5fd;
}
.hidden-eye span::after {
top: 5px;
left: -2px;
@@ -1804,7 +1813,8 @@ input[type="range"] {
grid-template-columns: minmax(0, 1fr) 30px;
gap: 12px;
align-items: center;
padding: 10px;
min-height: 38px;
padding: 7px 10px;
cursor: pointer;
transition: border-color 0.16s ease, background 0.16s ease, opacity 0.16s ease;
}
@@ -1818,7 +1828,7 @@ input[type="range"] {
display: block;
overflow: hidden;
color: #e8f1fb;
font-size: 13px;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
@@ -1828,12 +1838,12 @@ input[type="range"] {
}
.stl-family {
margin-bottom: 10px;
margin-bottom: 7px;
}
.stl-family-head {
width: 100%;
min-height: 32px;
min-height: 30px;
display: grid;
grid-template-columns: minmax(0, 1fr) auto 14px;
align-items: center;
@@ -1885,8 +1895,8 @@ input[type="range"] {
}
.stl-hierarchy-group .stl-family-head {
min-height: 36px;
margin-bottom: 6px;
min-height: 32px;
margin-bottom: 5px;
border-color: rgba(148, 163, 184, 0.22);
border-radius: 7px;
background: linear-gradient(180deg, rgba(31, 41, 59, 0.94), rgba(15, 23, 42, 0.94));
@@ -1899,20 +1909,20 @@ input[type="range"] {
}
.stl-hierarchy-group .stl-family-list {
margin: -2px 0 10px 12px;
padding-left: 10px;
margin: -2px 0 8px 12px;
padding-left: 8px;
border-left: 1px solid rgba(45, 212, 191, 0.22);
}
.stl-hierarchy-group .stl-row {
min-height: 58px;
margin-bottom: 7px;
border-radius: 10px;
min-height: 38px;
margin-bottom: 5px;
border-radius: 8px;
}
.stl-visibility-swatch {
width: 24px;
height: 24px;
width: 22px;
height: 22px;
justify-self: end;
border: 2px solid rgba(226, 232, 240, 0.68);
border-radius: 5px;
@@ -1940,9 +1950,9 @@ input[type="range"] {
.stl-visibility-swatch.hidden::after {
content: "";
display: block;
width: 26px;
width: 24px;
height: 2px;
margin: 9px 0 0 -3px;
margin: 8px 0 0 -3px;
border-radius: 99px;
background: rgba(226, 232, 240, 0.76);
transform: rotate(-45deg);