Compact STL list and clarify auto iteration
This commit is contained in:
@@ -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,27 +3285,40 @@ 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)) };
|
||||
};
|
||||
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} -` });
|
||||
});
|
||||
for (let left = 0; left < enabledKeys.length; left += 1) {
|
||||
for (let right = left + 1; right < enabledKeys.length; right += 1) {
|
||||
[-1, 1].forEach((leftSign) => {
|
||||
[-1, 1].forEach((rightSign) => {
|
||||
const leftKey = enabledKeys[left];
|
||||
const rightKey = enabledKeys[right];
|
||||
const pose = poseWithDelta(
|
||||
poseWithDelta(best.pose, leftKey, stepForKey(leftKey, 0.65) * leftSign),
|
||||
rightKey,
|
||||
stepForKey(rightKey, 0.65) * rightSign,
|
||||
);
|
||||
candidates.push({ pose, mode: `${leftKey}/${rightKey} 联合` });
|
||||
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) => {
|
||||
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) {
|
||||
[-1, 1].forEach((leftSign) => {
|
||||
[-1, 1].forEach((rightSign) => {
|
||||
const leftKey = enabledKeys[left];
|
||||
const rightKey = enabledKeys[right];
|
||||
const pose = poseWithDelta(
|
||||
poseWithDelta(base.pose, leftKey, stepForKey(leftKey, 0.65) * leftSign),
|
||||
rightKey,
|
||||
stepForKey(rightKey, 0.65) * rightSign,
|
||||
);
|
||||
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"));
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user