Refine registration DICOM selection and model centering
This commit is contained in:
@@ -57,6 +57,7 @@ const state = {
|
|||||||
activeCase: null,
|
activeCase: null,
|
||||||
series: [],
|
series: [],
|
||||||
selectedSeriesUid: "",
|
selectedSeriesUid: "",
|
||||||
|
registrationSeriesUid: "",
|
||||||
stlFiles: [],
|
stlFiles: [],
|
||||||
selectedStlIds: new Set(),
|
selectedStlIds: new Set(),
|
||||||
algorithmModel: "",
|
algorithmModel: "",
|
||||||
@@ -460,6 +461,7 @@ function clearDetail() {
|
|||||||
state.stlFiles = [];
|
state.stlFiles = [];
|
||||||
state.selectedStlIds = new Set();
|
state.selectedStlIds = new Set();
|
||||||
state.selectedSeriesUid = "";
|
state.selectedSeriesUid = "";
|
||||||
|
state.registrationSeriesUid = "";
|
||||||
$("activeTitle").textContent = "未选择 CT";
|
$("activeTitle").textContent = "未选择 CT";
|
||||||
$("activeMeta").textContent = "从左侧选择一个完整关联检查";
|
$("activeMeta").textContent = "从左侧选择一个完整关联检查";
|
||||||
$("activeTags").innerHTML = "";
|
$("activeTags").innerHTML = "";
|
||||||
@@ -513,6 +515,7 @@ async function selectCase(ctNumber, algorithmModel = "未指定模型") {
|
|||||||
const defaultSeries = pickDefaultSeries(state.series);
|
const defaultSeries = pickDefaultSeries(state.series);
|
||||||
const savedSeries = state.series.find((item) => item.series_uid === registration.series_instance_uid);
|
const savedSeries = state.series.find((item) => item.series_uid === registration.series_instance_uid);
|
||||||
const shouldUseSavedSeries = registration.registration_status === "registered" && savedSeries;
|
const shouldUseSavedSeries = registration.registration_status === "registered" && savedSeries;
|
||||||
|
state.registrationSeriesUid = savedSeries?.series_uid || "";
|
||||||
state.selectedSeriesUid = shouldUseSavedSeries ? savedSeries.series_uid : defaultSeries?.series_uid || "";
|
state.selectedSeriesUid = shouldUseSavedSeries ? savedSeries.series_uid : defaultSeries?.series_uid || "";
|
||||||
const selected = currentSeries();
|
const selected = currentSeries();
|
||||||
state.sliceIndex = selected ? Math.max(0, Math.floor((Number(selected.count) || 1) / 2)) : 0;
|
state.sliceIndex = selected ? Math.max(0, Math.floor((Number(selected.count) || 1) / 2)) : 0;
|
||||||
@@ -583,14 +586,15 @@ function renderActiveHeader() {
|
|||||||
const row = state.activeCase;
|
const row = state.activeCase;
|
||||||
if (!row) return;
|
if (!row) return;
|
||||||
const selected = currentSeries();
|
const selected = currentSeries();
|
||||||
|
const registration = registrationSeries();
|
||||||
const bodyTags = bodyPartTags(row);
|
const bodyTags = bodyPartTags(row);
|
||||||
const selectedLabels = selected?.annotation_labels?.length ? selected.annotation_labels.filter((label) => !String(label).includes("略过") && !String(label).includes("不采用")) : [];
|
const selectedLabels = registration?.annotation_labels?.length ? registration.annotation_labels.filter((label) => !String(label).includes("略过") && !String(label).includes("不采用")) : [];
|
||||||
$("activeTitle").textContent = `${row.ct_number} · ${row.algorithm_model || "未指定模型"}`;
|
$("activeTitle").textContent = `${row.ct_number} · ${row.algorithm_model || "未指定模型"}`;
|
||||||
$("activeMeta").textContent = `${row.patient_name || row.upp_patient_name || "-"} · ${row.patient_id || "-"} · ${fmtDateTime(row.study_date, row.study_time) || "检查时间未知"} · ${bodyTags.join("、") || row.study_description || "部位未知"} · ${Number(row.series_count || 0)} 个 DICOM 序列 · STL ${Number(row.stl_file_count || 0)} 个`;
|
$("activeMeta").textContent = `${row.patient_name || row.upp_patient_name || "-"} · ${row.patient_id || "-"} · ${fmtDateTime(row.study_date, row.study_time) || "检查时间未知"} · ${bodyTags.join("、") || row.study_description || "部位未知"} · ${Number(row.series_count || 0)} 个 DICOM 序列 · STL ${Number(row.stl_file_count || 0)} 个`;
|
||||||
const tags = [
|
const tags = [
|
||||||
`<span class="tag ${state.registrationStatus === "registered" ? "green" : "amber"}">${statusText(state.registrationStatus)}</span>`,
|
`<span class="tag ${state.registrationStatus === "registered" ? "green" : "amber"}">${statusText(state.registrationStatus)}</span>`,
|
||||||
`<span class="tag">${escapeHtml(state.algorithmModel || row.algorithm_model || "未指定模型")}</span>`,
|
`<span class="tag">${escapeHtml(state.algorithmModel || row.algorithm_model || "未指定模型")}</span>`,
|
||||||
...(selected ? [`<span class="tag">${escapeHtml(selected.description || "当前序列")}</span>`] : []),
|
...(registration ? [`<span class="tag">配准 ${escapeHtml(registration.description || "当前序列")}</span>`] : []),
|
||||||
...selectedLabels.map((tag) => `<span class="tag">${escapeHtml(tag)}</span>`),
|
...selectedLabels.map((tag) => `<span class="tag">${escapeHtml(tag)}</span>`),
|
||||||
...bodyTags.map((tag) => `<span class="tag">${escapeHtml(tag)}</span>`),
|
...bodyTags.map((tag) => `<span class="tag">${escapeHtml(tag)}</span>`),
|
||||||
];
|
];
|
||||||
@@ -603,23 +607,37 @@ function renderSeries() {
|
|||||||
$("seriesCount").textContent = state.series.length;
|
$("seriesCount").textContent = state.series.length;
|
||||||
$("seriesList").innerHTML = state.series.length
|
$("seriesList").innerHTML = state.series.length
|
||||||
? sortedSeriesForDisplay(state.series)
|
? sortedSeriesForDisplay(state.series)
|
||||||
.map((item) => seriesCardHtml(item, item.series_uid === state.selectedSeriesUid))
|
.map((item) => seriesCardHtml(item, item.series_uid === state.selectedSeriesUid, item.series_uid === state.registrationSeriesUid))
|
||||||
.join("")
|
.join("")
|
||||||
: `<div class="empty-state">未读取到 DICOM 序列</div>`;
|
: `<div class="empty-state">未读取到 DICOM 序列</div>`;
|
||||||
document.querySelectorAll(".series-card[data-series]").forEach((button) => {
|
document.querySelectorAll(".series-card[data-series]").forEach((card) => {
|
||||||
button.addEventListener("click", () => selectSeries(button.dataset.series));
|
card.addEventListener("click", (event) => {
|
||||||
|
if (event.target.closest(".series-check")) return;
|
||||||
|
selectSeries(card.dataset.series);
|
||||||
|
});
|
||||||
|
card.addEventListener("keydown", (event) => {
|
||||||
|
if (!["Enter", " "].includes(event.key)) return;
|
||||||
|
event.preventDefault();
|
||||||
|
selectSeries(card.dataset.series);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
document.querySelectorAll(".series-check[data-registration-series]").forEach((button) => {
|
||||||
|
button.addEventListener("click", async (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
await setRegistrationSeries(button.dataset.registrationSeries);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
updateSliceControl();
|
updateSliceControl();
|
||||||
renderDicomAnnotation();
|
renderDicomAnnotation();
|
||||||
}
|
}
|
||||||
|
|
||||||
function seriesCardHtml(item, active = false) {
|
function seriesCardHtml(item, active = false, registrationSelected = false) {
|
||||||
const labels = (item.annotation_labels || []).map((label) => `<i>${escapeHtml(label)}</i>`).join("");
|
const labels = (item.annotation_labels || []).map((label) => `<i>${escapeHtml(label)}</i>`).join("");
|
||||||
const time = [fmtTime(item.first_time || item.series_time), fmtTime(item.last_time)].filter(Boolean);
|
const time = [fmtTime(item.first_time || item.series_time), fmtTime(item.last_time)].filter(Boolean);
|
||||||
const skipped = isSkippedSeries(item);
|
const skipped = isSkippedSeries(item);
|
||||||
return `
|
return `
|
||||||
<button class="series-card ${active ? "active" : ""} ${skipped ? "skipped" : ""}" data-series="${escapeHtml(item.series_uid)}" title="${escapeHtml(item.description)}">
|
<div class="series-card ${active ? "active" : ""} ${registrationSelected ? "registration-selected" : ""} ${skipped ? "skipped" : ""}" data-series="${escapeHtml(item.series_uid)}" title="${escapeHtml(item.description)}" role="button" tabindex="0">
|
||||||
<span class="series-check" aria-hidden="true">${active ? "✓" : ""}</span>
|
<button class="series-check" data-registration-series="${escapeHtml(item.series_uid)}" type="button" aria-label="设为配准 DICOM">${registrationSelected ? "✓" : ""}</button>
|
||||||
<strong>${escapeHtml(item.description || "未命名序列")}</strong>
|
<strong>${escapeHtml(item.description || "未命名序列")}</strong>
|
||||||
<span>拍摄 ${escapeHtml(time.length > 1 ? `${time[0]}-${time[1]}` : time[0] || "-")}</span>
|
<span>拍摄 ${escapeHtml(time.length > 1 ? `${time[0]}-${time[1]}` : time[0] || "-")}</span>
|
||||||
<small>${escapeHtml(item.slice_thickness || "厚度未知")} · 序列 ${escapeHtml(item.series_number || "-")} · ${escapeHtml(item.rows || "-")}×${escapeHtml(item.columns || "-")} · ${escapeHtml(item.modality || "CT")} · ${Number(item.count || 0)} 张</small>
|
<small>${escapeHtml(item.slice_thickness || "厚度未知")} · 序列 ${escapeHtml(item.series_number || "-")} · ${escapeHtml(item.rows || "-")}×${escapeHtml(item.columns || "-")} · ${escapeHtml(item.modality || "CT")} · ${Number(item.count || 0)} 张</small>
|
||||||
@@ -627,20 +645,39 @@ function seriesCardHtml(item, active = false) {
|
|||||||
${item.body_part_dicom ? `<i>${escapeHtml(item.body_part_dicom)}</i>` : ""}
|
${item.body_part_dicom ? `<i>${escapeHtml(item.body_part_dicom)}</i>` : ""}
|
||||||
${labels || `<i>未标注</i>`}
|
${labels || `<i>未标注</i>`}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectSeries(uid) {
|
function registrationSeries() {
|
||||||
|
return state.series.find((item) => item.series_uid === state.registrationSeriesUid) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setRegistrationSeries(uid) {
|
||||||
|
if (!uid) return;
|
||||||
|
if (uid !== state.selectedSeriesUid) {
|
||||||
|
await selectSeries(uid, { markRegistration: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.registrationSeriesUid = uid;
|
||||||
|
markDirty("series");
|
||||||
|
renderSeries();
|
||||||
|
renderActiveHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectSeries(uid, options = {}) {
|
||||||
if (!uid || uid === state.selectedSeriesUid) return;
|
if (!uid || uid === state.selectedSeriesUid) return;
|
||||||
if (!(await confirmChange())) return;
|
if (!(await confirmChange())) return;
|
||||||
state.selectedSeriesUid = uid;
|
state.selectedSeriesUid = uid;
|
||||||
|
if (options.markRegistration) state.registrationSeriesUid = uid;
|
||||||
const selected = currentSeries();
|
const selected = currentSeries();
|
||||||
state.sliceIndex = selected ? Math.max(0, Math.floor((Number(selected.count) || 1) / 2)) : 0;
|
state.sliceIndex = selected ? Math.max(0, Math.floor((Number(selected.count) || 1) / 2)) : 0;
|
||||||
state.sliceRangeStart = 0;
|
state.sliceRangeStart = 0;
|
||||||
state.sliceRangeEnd = selected ? Math.max(0, Number(selected.count || 1) - 1) : 0;
|
state.sliceRangeEnd = selected ? Math.max(0, Number(selected.count || 1) - 1) : 0;
|
||||||
setSaveState("序列未保存", "warn");
|
if (options.markRegistration) markDirty("series");
|
||||||
|
else setSaveState("正在浏览序列", "");
|
||||||
renderSeries();
|
renderSeries();
|
||||||
|
renderActiveHeader();
|
||||||
renderDicomAnnotation();
|
renderDicomAnnotation();
|
||||||
await loadFusion();
|
await loadFusion();
|
||||||
}
|
}
|
||||||
@@ -671,7 +708,7 @@ function renderStl() {
|
|||||||
const id = Number(input.dataset.stl);
|
const id = Number(input.dataset.stl);
|
||||||
if (input.checked) state.selectedStlIds.add(id);
|
if (input.checked) state.selectedStlIds.add(id);
|
||||||
else state.selectedStlIds.delete(id);
|
else state.selectedStlIds.delete(id);
|
||||||
setSaveState("模型未保存", "warn");
|
markDirty("stl");
|
||||||
renderStl();
|
renderStl();
|
||||||
await loadFusion();
|
await loadFusion();
|
||||||
});
|
});
|
||||||
@@ -679,6 +716,27 @@ function renderStl() {
|
|||||||
renderAutoBoneOptions();
|
renderAutoBoneOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function selectAllStl() {
|
||||||
|
if (!state.stlFiles.length) return;
|
||||||
|
state.selectedStlIds = new Set(state.stlFiles.map((file) => Number(file.id)).filter(Number.isFinite));
|
||||||
|
markDirty("stl");
|
||||||
|
renderStl();
|
||||||
|
await loadFusion();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function invertStlSelection() {
|
||||||
|
if (!state.stlFiles.length) return;
|
||||||
|
const next = new Set();
|
||||||
|
state.stlFiles.forEach((file) => {
|
||||||
|
const id = Number(file.id);
|
||||||
|
if (Number.isFinite(id) && !state.selectedStlIds.has(id)) next.add(id);
|
||||||
|
});
|
||||||
|
state.selectedStlIds = next;
|
||||||
|
markDirty("stl");
|
||||||
|
renderStl();
|
||||||
|
await loadFusion();
|
||||||
|
}
|
||||||
|
|
||||||
function stlLikelyScore(file) {
|
function stlLikelyScore(file) {
|
||||||
const textValue = `${file.family || ""} ${file.segment_name || ""} ${file.file_name || ""} ${file.category || ""}`.toLowerCase();
|
const textValue = `${file.family || ""} ${file.segment_name || ""} ${file.file_name || ""} ${file.category || ""}`.toLowerCase();
|
||||||
const priority = [
|
const priority = [
|
||||||
@@ -1014,6 +1072,11 @@ function clearGroup(group) {
|
|||||||
function clearFusion() {
|
function clearFusion() {
|
||||||
clearGroup(state.dicomGroup);
|
clearGroup(state.dicomGroup);
|
||||||
clearGroup(state.rawModelGroup);
|
clearGroup(state.rawModelGroup);
|
||||||
|
if (state.rawModelGroup) {
|
||||||
|
state.rawModelGroup.position.set(0, 0, 0);
|
||||||
|
state.rawModelGroup.rotation.set(0, 0, 0);
|
||||||
|
state.rawModelGroup.scale.set(1, 1, 1);
|
||||||
|
}
|
||||||
state.volumeMeta = null;
|
state.volumeMeta = null;
|
||||||
state.volumeScene = null;
|
state.volumeScene = null;
|
||||||
state.modelMeshes = [];
|
state.modelMeshes = [];
|
||||||
@@ -1571,15 +1634,49 @@ async function buildDicomVolume(volume) {
|
|||||||
state.dicomSceneBox = new THREE.Box3().setFromObject(state.dicomGroup);
|
state.dicomSceneBox = new THREE.Box3().setFromObject(state.dicomGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateBaseModelScaleFromBounds() {
|
||||||
|
const size = state.modelLocalBounds?.size;
|
||||||
|
if (!size) return;
|
||||||
|
const maxModelSize = Math.max(Number(size.x) || 0, Number(size.y) || 0, Number(size.z) || 0, 1);
|
||||||
|
const volumeSize = state.volumeScene
|
||||||
|
? Math.max(state.volumeScene.width, state.volumeScene.height, state.volumeScene.depth, 1)
|
||||||
|
: 4.8;
|
||||||
|
state.baseModelScale = (volumeSize / maxModelSize) * 0.92;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addModelBoundsFrame(size) {
|
||||||
|
if (!state.rawModelGroup || !size) return;
|
||||||
|
const frame = new THREE.LineSegments(
|
||||||
|
new THREE.EdgesGeometry(new THREE.BoxGeometry(size.x, size.y, size.z)),
|
||||||
|
new THREE.LineBasicMaterial({
|
||||||
|
color: 0xfacc15,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.96,
|
||||||
|
depthTest: false,
|
||||||
|
depthWrite: false,
|
||||||
|
toneMapped: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
frame.name = "model-bounds";
|
||||||
|
frame.renderOrder = 1000;
|
||||||
|
state.rawModelGroup.add(frame);
|
||||||
|
state.modelBoundsFrame = frame;
|
||||||
|
}
|
||||||
|
|
||||||
async function buildStlModels(volume, onProgress = null) {
|
async function buildStlModels(volume, onProgress = null) {
|
||||||
const signature = stlSelectionSignature();
|
const signature = stlSelectionSignature();
|
||||||
if (signature && signature === state.stlSignature && state.rawModelGroup?.children.length) {
|
if (signature && signature === state.stlSignature && state.rawModelGroup?.children.length) {
|
||||||
|
updateBaseModelScaleFromBounds();
|
||||||
applyPose();
|
applyPose();
|
||||||
applyModelDetail();
|
applyModelDetail();
|
||||||
|
if (!state.modelBoundsFrame && state.modelLocalBounds?.size) addModelBoundsFrame(state.modelLocalBounds.size);
|
||||||
scheduleMappingDraw();
|
scheduleMappingDraw();
|
||||||
onProgress?.(100);
|
onProgress?.(100);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
state.rawModelGroup.position.set(0, 0, 0);
|
||||||
|
state.rawModelGroup.rotation.set(0, 0, 0);
|
||||||
|
state.rawModelGroup.scale.set(1, 1, 1);
|
||||||
clearGroup(state.rawModelGroup);
|
clearGroup(state.rawModelGroup);
|
||||||
state.modelMeshes = [];
|
state.modelMeshes = [];
|
||||||
state.modelBoundsFrame = null;
|
state.modelBoundsFrame = null;
|
||||||
@@ -1622,35 +1719,28 @@ async function buildStlModels(volume, onProgress = null) {
|
|||||||
state.modelMeshes.push({ mesh, file, color: cssColor(index), index });
|
state.modelMeshes.push({ mesh, file, color: cssColor(index), index });
|
||||||
onProgress?.(((index + 1) / Math.max(files.length, 1)) * 100);
|
onProgress?.(((index + 1) / Math.max(files.length, 1)) * 100);
|
||||||
}
|
}
|
||||||
const bbox = new THREE.Box3().setFromObject(state.rawModelGroup);
|
const bbox = new THREE.Box3();
|
||||||
|
state.modelMeshes.forEach((record) => {
|
||||||
|
record.mesh.geometry.computeBoundingBox?.();
|
||||||
|
if (record.mesh.geometry.boundingBox) bbox.union(record.mesh.geometry.boundingBox);
|
||||||
|
});
|
||||||
const center = new THREE.Vector3();
|
const center = new THREE.Vector3();
|
||||||
const size = new THREE.Vector3();
|
const size = new THREE.Vector3();
|
||||||
bbox.getCenter(center);
|
bbox.getCenter(center);
|
||||||
bbox.getSize(size);
|
bbox.getSize(size);
|
||||||
state.rawModelGroup.position.set(0, 0, 0);
|
state.modelMeshes.forEach((record) => {
|
||||||
state.rawModelGroup.traverse((object) => {
|
const geometry = record.mesh.geometry;
|
||||||
if (!object.geometry) return;
|
geometry.translate(-center.x, -center.y, -center.z);
|
||||||
object.geometry.translate(-center.x, -center.y, -center.z);
|
geometry.computeBoundingBox?.();
|
||||||
object.geometry.computeBoundingBox?.();
|
geometry.computeBoundingSphere?.();
|
||||||
object.geometry.computeBoundingSphere?.();
|
geometry.computeVertexNormals?.();
|
||||||
object.geometry.computeVertexNormals?.();
|
|
||||||
});
|
});
|
||||||
const maxModelSize = Math.max(size.x, size.y, size.z, 1);
|
|
||||||
const volumeSize = state.volumeScene
|
|
||||||
? Math.max(state.volumeScene.width, state.volumeScene.height, state.volumeScene.depth, 1)
|
|
||||||
: 4.8;
|
|
||||||
state.baseModelScale = (volumeSize / maxModelSize) * 0.92;
|
|
||||||
state.modelLocalBounds = {
|
state.modelLocalBounds = {
|
||||||
center: { x: center.x, y: center.y, z: center.z },
|
center: { x: center.x, y: center.y, z: center.z },
|
||||||
size: { x: size.x, y: size.y, z: size.z },
|
size: { x: size.x, y: size.y, z: size.z },
|
||||||
};
|
};
|
||||||
const modelBounds = new THREE.LineSegments(
|
updateBaseModelScaleFromBounds();
|
||||||
new THREE.EdgesGeometry(new THREE.BoxGeometry(size.x, size.y, size.z)),
|
addModelBoundsFrame(size);
|
||||||
new THREE.LineBasicMaterial({ color: 0xfacc15, transparent: true, opacity: 0.78 }),
|
|
||||||
);
|
|
||||||
modelBounds.name = "model-bounds";
|
|
||||||
state.rawModelGroup.add(modelBounds);
|
|
||||||
state.modelBoundsFrame = modelBounds;
|
|
||||||
applyPose();
|
applyPose();
|
||||||
scheduleMappingDraw();
|
scheduleMappingDraw();
|
||||||
onProgress?.(100);
|
onProgress?.(100);
|
||||||
@@ -1948,11 +2038,21 @@ async function saveRegistration(nextStatus = null) {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
state.registrationStatus = status;
|
state.registrationStatus = status;
|
||||||
if (state.activeCase) state.activeCase.registration_status = status;
|
state.registrationSeriesUid = selectedSeries.series_uid;
|
||||||
|
if (state.activeCase) {
|
||||||
|
state.activeCase.registration_status = status;
|
||||||
|
state.activeCase.series_instance_uid = selectedSeries.series_uid;
|
||||||
|
state.activeCase.series_description = selectedSeries.description || "";
|
||||||
|
}
|
||||||
const existing = state.cases.find((row) => sameCase(row, state.activeCase));
|
const existing = state.cases.find((row) => sameCase(row, state.activeCase));
|
||||||
if (existing) existing.registration_status = status;
|
if (existing) {
|
||||||
|
existing.registration_status = status;
|
||||||
|
existing.series_instance_uid = selectedSeries.series_uid;
|
||||||
|
existing.series_description = selectedSeries.description || "";
|
||||||
|
}
|
||||||
resetDirty();
|
resetDirty();
|
||||||
renderCases();
|
renderCases();
|
||||||
|
renderSeries();
|
||||||
renderActiveHeader();
|
renderActiveHeader();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSaveState(error.message, "error");
|
setSaveState(error.message, "error");
|
||||||
@@ -2038,6 +2138,8 @@ function wireEvents() {
|
|||||||
$("saveBtn").addEventListener("click", () => saveRegistration());
|
$("saveBtn").addEventListener("click", () => saveRegistration());
|
||||||
$("statusBtn").addEventListener("click", () => saveRegistration(state.registrationStatus === "registered" ? "unregistered" : "registered"));
|
$("statusBtn").addEventListener("click", () => saveRegistration(state.registrationStatus === "registered" ? "unregistered" : "registered"));
|
||||||
$("notes").addEventListener("input", () => markDirty("notes"));
|
$("notes").addEventListener("input", () => markDirty("notes"));
|
||||||
|
$("selectAllStlBtn").addEventListener("click", selectAllStl);
|
||||||
|
$("invertStlBtn").addEventListener("click", invertStlSelection);
|
||||||
$("resetRotationBtn").addEventListener("click", () => resetPose("rotation"));
|
$("resetRotationBtn").addEventListener("click", () => resetPose("rotation"));
|
||||||
$("resetTransformBtn").addEventListener("click", () => resetPose("transform"));
|
$("resetTransformBtn").addEventListener("click", () => resetPose("transform"));
|
||||||
$("resetFlipBtn").addEventListener("click", () => resetPose("flip"));
|
$("resetFlipBtn").addEventListener("click", () => resetPose("flip"));
|
||||||
|
|||||||
@@ -170,6 +170,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="tool-section-title compact-title">
|
<div class="tool-section-title compact-title">
|
||||||
<span>STL</span>
|
<span>STL</span>
|
||||||
|
<div class="title-actions">
|
||||||
|
<button id="selectAllStlBtn" type="button">全选</button>
|
||||||
|
<button id="invertStlBtn" type="button">反选</button>
|
||||||
|
</div>
|
||||||
<em id="stlCount">0</em>
|
<em id="stlCount">0</em>
|
||||||
</div>
|
</div>
|
||||||
<div id="modelRail" class="chip-rail"></div>
|
<div id="modelRail" class="chip-rail"></div>
|
||||||
|
|||||||
@@ -499,30 +499,49 @@ button {
|
|||||||
.series-card {
|
.series-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-right: 48px;
|
padding-right: 48px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.series-check {
|
.series-check {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 13px;
|
top: 13px;
|
||||||
right: 13px;
|
right: 13px;
|
||||||
width: 26px;
|
width: 30px;
|
||||||
height: 26px;
|
height: 30px;
|
||||||
display: inline-flex !important;
|
display: inline-flex !important;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
border: 1px solid rgba(25, 214, 195, 0.62);
|
padding: 0;
|
||||||
|
border: 2px solid rgba(45, 212, 191, 0.9);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: rgba(20, 184, 166, 0.16);
|
background: rgba(13, 148, 136, 0.22);
|
||||||
color: #ccfbf1 !important;
|
color: #ccfbf1 !important;
|
||||||
font-size: 15px !important;
|
font-size: 15px !important;
|
||||||
font-weight: 950 !important;
|
font-weight: 950 !important;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 0 0 1px rgba(6, 182, 212, 0.18), 0 0 18px rgba(20, 184, 166, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.series-card:not(.active) .series-check {
|
.series-card:not(.registration-selected) .series-check {
|
||||||
border-color: rgba(148, 163, 184, 0.2);
|
border-color: rgba(148, 163, 184, 0.42);
|
||||||
background: rgba(148, 163, 184, 0.06);
|
background: rgba(15, 23, 42, 0.82);
|
||||||
|
color: transparent !important;
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-card:not(.registration-selected) .series-check:hover,
|
||||||
|
.series-check:focus-visible {
|
||||||
|
border-color: rgba(45, 212, 191, 0.95);
|
||||||
|
background: rgba(20, 184, 166, 0.18);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-card.registration-selected .series-check {
|
||||||
|
border-color: rgba(153, 246, 228, 0.96);
|
||||||
|
background: linear-gradient(180deg, rgba(20, 184, 166, 0.96), rgba(13, 148, 136, 0.82));
|
||||||
|
color: #ecfeff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.case-card.active,
|
.case-card.active,
|
||||||
@@ -532,6 +551,10 @@ button {
|
|||||||
background: linear-gradient(180deg, rgba(20, 45, 88, 0.96), rgba(10, 18, 31, 0.96));
|
background: linear-gradient(180deg, rgba(20, 45, 88, 0.96), rgba(10, 18, 31, 0.96));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.series-card.registration-selected {
|
||||||
|
border-color: rgba(20, 184, 166, 0.82);
|
||||||
|
}
|
||||||
|
|
||||||
.series-card.skipped {
|
.series-card.skipped {
|
||||||
opacity: 0.54;
|
opacity: 0.54;
|
||||||
filter: grayscale(0.95);
|
filter: grayscale(0.95);
|
||||||
@@ -754,11 +777,27 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tool-section-title button {
|
.tool-section-title button {
|
||||||
border: 0;
|
border: 1px solid rgba(45, 212, 191, 0.34);
|
||||||
background: transparent;
|
border-radius: 8px;
|
||||||
color: #c4d7ee;
|
background: rgba(8, 17, 26, 0.62);
|
||||||
|
color: #d9fbff;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 6px 9px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-section-title button:hover {
|
||||||
|
border-color: rgba(45, 212, 191, 0.76);
|
||||||
|
background: rgba(20, 184, 166, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-section-title em {
|
.tool-section-title em {
|
||||||
|
|||||||
Reference in New Issue
Block a user