Improve registration fusion interaction
This commit is contained in:
@@ -996,6 +996,8 @@ def dicom_fusion_volume(
|
|||||||
series_uid: str,
|
series_uid: str,
|
||||||
center_index: int = 0,
|
center_index: int = 0,
|
||||||
window: str = "soft",
|
window: str = "soft",
|
||||||
|
detail: str = "high",
|
||||||
|
texture_size: int | None = Query(default=None, ge=128, le=1024),
|
||||||
radius: int = Query(default=96, ge=4, le=512),
|
radius: int = Query(default=96, ge=4, le=512),
|
||||||
range_start: int | None = None,
|
range_start: int | None = None,
|
||||||
range_end: int | None = None,
|
range_end: int | None = None,
|
||||||
@@ -1014,19 +1016,22 @@ def dicom_fusion_volume(
|
|||||||
else:
|
else:
|
||||||
start = max(0, center_index - radius)
|
start = max(0, center_index - radius)
|
||||||
end = min(total - 1, center_index + radius)
|
end = min(total - 1, center_index + radius)
|
||||||
max_frames = 72
|
max_frames = {"low": 48, "medium": 64, "high": 80}.get(str(detail).lower(), 64)
|
||||||
if end - start + 1 > max_frames:
|
if end - start + 1 > max_frames:
|
||||||
step = int(np.ceil((end - start + 1) / max_frames))
|
step = int(np.ceil((end - start + 1) / max_frames))
|
||||||
indices = list(range(start, end + 1, step))
|
indices = list(range(start, end + 1, step))
|
||||||
else:
|
else:
|
||||||
indices = list(range(start, end + 1))
|
indices = list(range(start, end + 1))
|
||||||
sample_ds = datasets[center_index]
|
focus_index = min(max(center_index, start), end)
|
||||||
|
indices = sorted({*indices, start, end, focus_index})
|
||||||
|
sample_ds = datasets[focus_index]
|
||||||
center, width = window_values(sample_ds, window)
|
center, width = window_values(sample_ds, window)
|
||||||
|
max_texture_size = int(texture_size or {"low": 384, "medium": 512, "high": 768}.get(str(detail).lower(), 512))
|
||||||
frames = []
|
frames = []
|
||||||
frame_width = 0
|
frame_width = 0
|
||||||
frame_height = 0
|
frame_height = 0
|
||||||
for index in indices:
|
for index in indices:
|
||||||
png = render_array(stack[index], center, width, max_size=256, spacing=(stack_data["row_spacing"], stack_data["col_spacing"]))
|
png = render_array(stack[index], center, width, max_size=max_texture_size, spacing=(stack_data["row_spacing"], stack_data["col_spacing"]))
|
||||||
frames.append("data:image/png;base64," + base64.b64encode(png).decode("ascii"))
|
frames.append("data:image/png;base64," + base64.b64encode(png).decode("ascii"))
|
||||||
if not frame_width:
|
if not frame_width:
|
||||||
with Image.open(io.BytesIO(png)) as pil:
|
with Image.open(io.BytesIO(png)) as pil:
|
||||||
@@ -1036,7 +1041,7 @@ def dicom_fusion_volume(
|
|||||||
"indices": indices,
|
"indices": indices,
|
||||||
"start": start,
|
"start": start,
|
||||||
"end": end,
|
"end": end,
|
||||||
"center": center_index,
|
"center": focus_index,
|
||||||
"total": total,
|
"total": total,
|
||||||
"width": frame_width,
|
"width": frame_width,
|
||||||
"height": frame_height,
|
"height": frame_height,
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ const state = {
|
|||||||
camera: null,
|
camera: null,
|
||||||
scene: null,
|
scene: null,
|
||||||
controls: null,
|
controls: null,
|
||||||
|
fusionRoot: null,
|
||||||
|
viewPose: { rotateX: 0, rotateZ: 0, translateX: 0, translateY: 0, scale: 1 },
|
||||||
|
sceneDrag: null,
|
||||||
dicomGroup: null,
|
dicomGroup: null,
|
||||||
dicomPlane: null,
|
dicomPlane: null,
|
||||||
dicomPoints: null,
|
dicomPoints: null,
|
||||||
@@ -985,10 +988,10 @@ function initScene() {
|
|||||||
const camera = new THREE.PerspectiveCamera(38, 1, 0.01, 1000);
|
const camera = new THREE.PerspectiveCamera(38, 1, 0.01, 1000);
|
||||||
camera.position.set(0, -8.5, 4.4);
|
camera.position.set(0, -8.5, 4.4);
|
||||||
camera.up.set(0, 0, 1);
|
camera.up.set(0, 0, 1);
|
||||||
|
camera.lookAt(0, 0, 0);
|
||||||
|
|
||||||
const controls = new OrbitControls(camera, renderer.domElement);
|
const controls = new OrbitControls(camera, renderer.domElement);
|
||||||
controls.enableDamping = true;
|
controls.enabled = false;
|
||||||
controls.dampingFactor = 0.08;
|
|
||||||
controls.target.set(0, 0, 0);
|
controls.target.set(0, 0, 0);
|
||||||
|
|
||||||
const ambient = new THREE.AmbientLight(0xffffff, 0.82);
|
const ambient = new THREE.AmbientLight(0xffffff, 0.82);
|
||||||
@@ -1000,10 +1003,12 @@ function initScene() {
|
|||||||
const modelGroup = new THREE.Group();
|
const modelGroup = new THREE.Group();
|
||||||
const rawModelGroup = new THREE.Group();
|
const rawModelGroup = new THREE.Group();
|
||||||
const axisGroup = createSceneAxes();
|
const axisGroup = createSceneAxes();
|
||||||
|
const fusionRoot = new THREE.Group();
|
||||||
modelGroup.add(rawModelGroup);
|
modelGroup.add(rawModelGroup);
|
||||||
scene.add(dicomGroup, modelGroup, axisGroup);
|
fusionRoot.add(dicomGroup, modelGroup, axisGroup);
|
||||||
|
scene.add(fusionRoot);
|
||||||
|
|
||||||
Object.assign(state, { renderer, camera, scene, controls, dicomGroup, modelGroup, rawModelGroup, axisGroup, sceneReady: true });
|
Object.assign(state, { renderer, camera, scene, controls, fusionRoot, dicomGroup, modelGroup, rawModelGroup, axisGroup, sceneReady: true });
|
||||||
|
|
||||||
const resize = () => {
|
const resize = () => {
|
||||||
const rect = viewport.getBoundingClientRect();
|
const rect = viewport.getBoundingClientRect();
|
||||||
@@ -1019,10 +1024,11 @@ function initScene() {
|
|||||||
|
|
||||||
const animate = () => {
|
const animate = () => {
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
controls.update();
|
applySceneViewPose();
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
};
|
};
|
||||||
animate();
|
animate();
|
||||||
|
wireFusionSceneDrag(renderer.domElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAxisLabel(text, color) {
|
function createAxisLabel(text, color) {
|
||||||
@@ -1065,11 +1071,66 @@ function createSceneAxes() {
|
|||||||
|
|
||||||
function initGeometryFallback() {
|
function initGeometryFallback() {
|
||||||
if (state.dicomGroup && state.modelGroup && state.rawModelGroup) return;
|
if (state.dicomGroup && state.modelGroup && state.rawModelGroup) return;
|
||||||
|
const fusionRoot = new THREE.Group();
|
||||||
const dicomGroup = new THREE.Group();
|
const dicomGroup = new THREE.Group();
|
||||||
const modelGroup = new THREE.Group();
|
const modelGroup = new THREE.Group();
|
||||||
const rawModelGroup = new THREE.Group();
|
const rawModelGroup = new THREE.Group();
|
||||||
modelGroup.add(rawModelGroup);
|
modelGroup.add(rawModelGroup);
|
||||||
Object.assign(state, { dicomGroup, modelGroup, rawModelGroup });
|
fusionRoot.add(dicomGroup, modelGroup);
|
||||||
|
Object.assign(state, { fusionRoot, dicomGroup, modelGroup, rawModelGroup });
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySceneViewPose() {
|
||||||
|
if (!state.fusionRoot) return;
|
||||||
|
const pose = state.viewPose || {};
|
||||||
|
state.fusionRoot.rotation.set(Number(pose.rotateX) || 0, 0, Number(pose.rotateZ) || 0);
|
||||||
|
state.fusionRoot.position.set(Number(pose.translateX) || 0, Number(pose.translateY) || 0, 0);
|
||||||
|
state.fusionRoot.scale.setScalar(Math.max(0.2, Number(pose.scale) || 1));
|
||||||
|
state.fusionRoot.updateMatrixWorld(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wireFusionSceneDrag(canvas) {
|
||||||
|
if (!canvas) return;
|
||||||
|
canvas.style.touchAction = "none";
|
||||||
|
canvas.addEventListener("contextmenu", (event) => event.preventDefault());
|
||||||
|
canvas.addEventListener("pointerdown", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
state.sceneDrag = {
|
||||||
|
pointerId: event.pointerId,
|
||||||
|
mode: event.button === 2 || event.shiftKey ? "pan" : "rotate",
|
||||||
|
startX: event.clientX,
|
||||||
|
startY: event.clientY,
|
||||||
|
pose: { ...state.viewPose },
|
||||||
|
};
|
||||||
|
canvas.setPointerCapture?.(event.pointerId);
|
||||||
|
});
|
||||||
|
canvas.addEventListener("pointermove", (event) => {
|
||||||
|
const drag = state.sceneDrag;
|
||||||
|
if (!drag || drag.pointerId !== event.pointerId) return;
|
||||||
|
event.preventDefault();
|
||||||
|
const dx = event.clientX - drag.startX;
|
||||||
|
const dy = event.clientY - drag.startY;
|
||||||
|
if (drag.mode === "pan") {
|
||||||
|
state.viewPose.translateX = drag.pose.translateX + dx * 0.006;
|
||||||
|
state.viewPose.translateY = drag.pose.translateY - dy * 0.006;
|
||||||
|
} else {
|
||||||
|
state.viewPose.rotateZ = drag.pose.rotateZ + dx * 0.008;
|
||||||
|
state.viewPose.rotateX = drag.pose.rotateX + dy * 0.008;
|
||||||
|
}
|
||||||
|
applySceneViewPose();
|
||||||
|
});
|
||||||
|
const stop = (event) => {
|
||||||
|
if (state.sceneDrag?.pointerId !== event.pointerId) return;
|
||||||
|
state.sceneDrag = null;
|
||||||
|
canvas.releasePointerCapture?.(event.pointerId);
|
||||||
|
};
|
||||||
|
canvas.addEventListener("pointerup", stop);
|
||||||
|
canvas.addEventListener("pointercancel", stop);
|
||||||
|
canvas.addEventListener("wheel", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
state.viewPose.scale = Math.max(0.35, Math.min(3, state.viewPose.scale - event.deltaY * 0.001));
|
||||||
|
applySceneViewPose();
|
||||||
|
}, { passive: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearGroup(group) {
|
function clearGroup(group) {
|
||||||
@@ -1129,10 +1190,10 @@ function updateFusionMeta() {
|
|||||||
|
|
||||||
function fusionDetailSettings() {
|
function fusionDetailSettings() {
|
||||||
return {
|
return {
|
||||||
low: { planeOpacity: 0.32, sliceOpacity: 0.05, slicePlanes: 12, pointOpacity: 0.18, pointSize: 0.014, pointGrid: 72, pointThreshold: 24 },
|
low: { planeOpacity: 0.82, sliceOpacity: 0.018, slicePlanes: 10, pointOpacity: 0.035, pointSize: 0.012, pointGrid: 72, pointThreshold: 36, textureSize: 384 },
|
||||||
medium: { planeOpacity: 0.46, sliceOpacity: 0.075, slicePlanes: 18, pointOpacity: 0.3, pointSize: 0.018, pointGrid: 88, pointThreshold: 22 },
|
medium: { planeOpacity: 0.92, sliceOpacity: 0.024, slicePlanes: 14, pointOpacity: 0.055, pointSize: 0.014, pointGrid: 88, pointThreshold: 34, textureSize: 512 },
|
||||||
high: { planeOpacity: 0.6, sliceOpacity: 0.1, slicePlanes: 26, pointOpacity: 0.42, pointSize: 0.021, pointGrid: 104, pointThreshold: 20 },
|
high: { planeOpacity: 1, sliceOpacity: 0.028, slicePlanes: 18, pointOpacity: 0.075, pointSize: 0.015, pointGrid: 104, pointThreshold: 32, textureSize: 768 },
|
||||||
}[state.fusionDetail] || { planeOpacity: 0.32, sliceOpacity: 0.05, slicePlanes: 12, pointOpacity: 0.18, pointSize: 0.014, pointGrid: 72, pointThreshold: 24 };
|
}[state.fusionDetail] || { planeOpacity: 0.82, sliceOpacity: 0.018, slicePlanes: 10, pointOpacity: 0.035, pointSize: 0.012, pointGrid: 72, pointThreshold: 36, textureSize: 384 };
|
||||||
}
|
}
|
||||||
|
|
||||||
function modelDetailSettings() {
|
function modelDetailSettings() {
|
||||||
@@ -1148,6 +1209,8 @@ function applyFusionDetail() {
|
|||||||
const detail = fusionDetailSettings();
|
const detail = fusionDetailSettings();
|
||||||
if (state.dicomPlane?.material) {
|
if (state.dicomPlane?.material) {
|
||||||
state.dicomPlane.material.opacity = detail.planeOpacity;
|
state.dicomPlane.material.opacity = detail.planeOpacity;
|
||||||
|
state.dicomPlane.material.transparent = true;
|
||||||
|
state.dicomPlane.material.depthWrite = false;
|
||||||
state.dicomPlane.material.needsUpdate = true;
|
state.dicomPlane.material.needsUpdate = true;
|
||||||
}
|
}
|
||||||
if (state.dicomPoints?.material) {
|
if (state.dicomPoints?.material) {
|
||||||
@@ -1212,6 +1275,8 @@ async function loadFusion() {
|
|||||||
series_uid: selected.series_uid,
|
series_uid: selected.series_uid,
|
||||||
center_index: String(state.sliceIndex),
|
center_index: String(state.sliceIndex),
|
||||||
window: state.windowMode,
|
window: state.windowMode,
|
||||||
|
detail: state.fusionDetail,
|
||||||
|
texture_size: String(fusionDetailSettings().textureSize),
|
||||||
range_start: String(state.sliceRangeStart),
|
range_start: String(state.sliceRangeStart),
|
||||||
range_end: String(state.sliceRangeEnd),
|
range_end: String(state.sliceRangeEnd),
|
||||||
});
|
});
|
||||||
@@ -1247,7 +1312,15 @@ async function loadFusion() {
|
|||||||
|
|
||||||
async function loadTexture(url) {
|
async function loadTexture(url) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
new THREE.TextureLoader().load(url, resolve, undefined, reject);
|
new THREE.TextureLoader().load(url, (texture) => {
|
||||||
|
texture.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
texture.generateMipmaps = false;
|
||||||
|
texture.minFilter = THREE.LinearFilter;
|
||||||
|
texture.magFilter = THREE.LinearFilter;
|
||||||
|
texture.anisotropy = Math.min(4, state.renderer?.capabilities?.getMaxAnisotropy?.() || 1);
|
||||||
|
texture.needsUpdate = true;
|
||||||
|
resolve(texture);
|
||||||
|
}, undefined, reject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1558,7 +1631,13 @@ async function buildDicomVolume(volume) {
|
|||||||
const total = Math.max(1, Number(volume.total || 1));
|
const total = Math.max(1, Number(volume.total || 1));
|
||||||
const indices = volume.indices || [];
|
const indices = volume.indices || [];
|
||||||
const frames = volume.frames || [];
|
const frames = volume.frames || [];
|
||||||
const centerPosition = indices.length ? Math.floor(indices.length / 2) : 0;
|
const centerPosition = indices.length
|
||||||
|
? indices.reduce((bestIndex, item, index) => {
|
||||||
|
const bestDistance = Math.abs(Number(indices[bestIndex] ?? centerIndex) - centerIndex);
|
||||||
|
const distance = Math.abs(Number(item ?? centerIndex) - centerIndex);
|
||||||
|
return distance < bestDistance ? index : bestIndex;
|
||||||
|
}, 0)
|
||||||
|
: 0;
|
||||||
state.volumeScene = {
|
state.volumeScene = {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@@ -1595,7 +1674,6 @@ async function buildDicomVolume(volume) {
|
|||||||
const fusionDetail = fusionDetailSettings();
|
const fusionDetail = fusionDetailSettings();
|
||||||
if (centerFrame) {
|
if (centerFrame) {
|
||||||
const texture = await loadTexture(centerFrame);
|
const texture = await loadTexture(centerFrame);
|
||||||
texture.colorSpace = THREE.SRGBColorSpace;
|
|
||||||
const plane = new THREE.Mesh(
|
const plane = new THREE.Mesh(
|
||||||
new THREE.PlaneGeometry(width, height),
|
new THREE.PlaneGeometry(width, height),
|
||||||
new THREE.MeshBasicMaterial({
|
new THREE.MeshBasicMaterial({
|
||||||
@@ -1604,9 +1682,11 @@ async function buildDicomVolume(volume) {
|
|||||||
opacity: fusionDetail.planeOpacity,
|
opacity: fusionDetail.planeOpacity,
|
||||||
side: THREE.DoubleSide,
|
side: THREE.DoubleSide,
|
||||||
depthWrite: false,
|
depthWrite: false,
|
||||||
|
depthTest: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
plane.position.z = sliceToSceneZ(centerIndex) + 0.006;
|
plane.position.z = sliceToSceneZ(centerIndex) + 0.006;
|
||||||
|
plane.renderOrder = 20;
|
||||||
state.dicomGroup.add(plane);
|
state.dicomGroup.add(plane);
|
||||||
state.dicomPlane = plane;
|
state.dicomPlane = plane;
|
||||||
}
|
}
|
||||||
@@ -1616,7 +1696,6 @@ async function buildDicomVolume(volume) {
|
|||||||
for (let frameIndex = 0; frameIndex < frames.length; frameIndex += planeStep) {
|
for (let frameIndex = 0; frameIndex < frames.length; frameIndex += planeStep) {
|
||||||
if (frameIndex === centerPosition) continue;
|
if (frameIndex === centerPosition) continue;
|
||||||
const texture = await loadTexture(frames[frameIndex]);
|
const texture = await loadTexture(frames[frameIndex]);
|
||||||
texture.colorSpace = THREE.SRGBColorSpace;
|
|
||||||
const plane = new THREE.Mesh(
|
const plane = new THREE.Mesh(
|
||||||
new THREE.PlaneGeometry(width, height),
|
new THREE.PlaneGeometry(width, height),
|
||||||
new THREE.MeshBasicMaterial({
|
new THREE.MeshBasicMaterial({
|
||||||
@@ -1629,6 +1708,7 @@ async function buildDicomVolume(volume) {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
plane.position.z = sliceToSceneZ(Number(indices[frameIndex] ?? centerIndex));
|
plane.position.z = sliceToSceneZ(Number(indices[frameIndex] ?? centerIndex));
|
||||||
|
plane.renderOrder = 4;
|
||||||
state.dicomGroup.add(plane);
|
state.dicomGroup.add(plane);
|
||||||
state.dicomSlicePlanes.push(plane);
|
state.dicomSlicePlanes.push(plane);
|
||||||
}
|
}
|
||||||
@@ -1667,6 +1747,7 @@ async function buildDicomVolume(volume) {
|
|||||||
depthWrite: false,
|
depthWrite: false,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
points.renderOrder = 2;
|
||||||
state.dicomGroup.add(points);
|
state.dicomGroup.add(points);
|
||||||
state.dicomPoints = points;
|
state.dicomPoints = points;
|
||||||
}
|
}
|
||||||
@@ -1809,15 +1890,17 @@ function applyPose(poseInput = state.pose) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fitCamera() {
|
function fitCamera() {
|
||||||
if (!state.camera || !state.controls) return;
|
if (!state.camera) return;
|
||||||
|
applySceneViewPose();
|
||||||
const box = new THREE.Box3();
|
const box = new THREE.Box3();
|
||||||
if (state.dicomGroup?.visible !== false) box.expandByObject(state.dicomGroup);
|
if (state.dicomGroup?.visible !== false) box.expandByObject(state.dicomGroup);
|
||||||
const modelBox = visibleModelBox();
|
const modelBox = visibleModelBox();
|
||||||
if (modelBox) box.union(modelBox);
|
if (modelBox) box.union(modelBox);
|
||||||
if (box.isEmpty()) {
|
if (box.isEmpty()) {
|
||||||
state.camera.position.set(0, -8.5, 4.4);
|
state.camera.position.set(0, -8.5, 4.4);
|
||||||
state.controls.target.set(0, 0, 0);
|
state.camera.lookAt(0, 0, 0);
|
||||||
state.controls.update();
|
state.controls?.target.set(0, 0, 0);
|
||||||
|
state.controls?.update();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const size = new THREE.Vector3();
|
const size = new THREE.Vector3();
|
||||||
@@ -1826,7 +1909,8 @@ function fitCamera() {
|
|||||||
box.getCenter(center);
|
box.getCenter(center);
|
||||||
const distance = Math.max(size.x, size.y, size.z, 1) * 1.8;
|
const distance = Math.max(size.x, size.y, size.z, 1) * 1.8;
|
||||||
state.camera.position.set(center.x, center.y - distance, center.z + distance * 0.45);
|
state.camera.position.set(center.x, center.y - distance, center.z + distance * 0.45);
|
||||||
state.controls.target.copy(center);
|
state.camera.lookAt(center);
|
||||||
|
state.controls?.target.copy(center);
|
||||||
if (state.axisGroup) {
|
if (state.axisGroup) {
|
||||||
const axisScale = Math.max(size.x, size.y, size.z, 1) / 5.5;
|
const axisScale = Math.max(size.x, size.y, size.z, 1) / 5.5;
|
||||||
state.axisGroup.scale.setScalar(axisScale);
|
state.axisGroup.scale.setScalar(axisScale);
|
||||||
@@ -1836,7 +1920,13 @@ function fitCamera() {
|
|||||||
center.z - size.z * 0.42,
|
center.z - size.z * 0.42,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
state.controls.update();
|
state.controls?.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSceneView() {
|
||||||
|
state.viewPose = { rotateX: 0, rotateZ: 0, translateX: 0, translateY: 0, scale: 1 };
|
||||||
|
applySceneViewPose();
|
||||||
|
fitCamera();
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetPose(kind) {
|
function resetPose(kind) {
|
||||||
@@ -2185,7 +2275,7 @@ function wireEvents() {
|
|||||||
$("manualCacheRefreshBtn").addEventListener("click", refreshCacheNow);
|
$("manualCacheRefreshBtn").addEventListener("click", refreshCacheNow);
|
||||||
$("relationBtn").addEventListener("click", () => openLinkedApp("relation"));
|
$("relationBtn").addEventListener("click", () => openLinkedApp("relation"));
|
||||||
$("viewerBtn").addEventListener("click", () => openLinkedApp("viewer"));
|
$("viewerBtn").addEventListener("click", () => openLinkedApp("viewer"));
|
||||||
$("fitBtn").addEventListener("click", fitCamera);
|
$("fitBtn").addEventListener("click", resetSceneView);
|
||||||
$("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"));
|
||||||
@@ -2328,11 +2418,14 @@ function wireEvents() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
document.querySelectorAll("[data-fusion-detail]").forEach((button) => {
|
document.querySelectorAll("[data-fusion-detail]").forEach((button) => {
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", async () => {
|
||||||
state.fusionDetail = button.dataset.fusionDetail || "low";
|
const nextDetail = button.dataset.fusionDetail || "low";
|
||||||
|
if (nextDetail === state.fusionDetail) return;
|
||||||
|
state.fusionDetail = nextDetail;
|
||||||
document.querySelectorAll("[data-fusion-detail]").forEach((item) => item.classList.toggle("active", item === button));
|
document.querySelectorAll("[data-fusion-detail]").forEach((item) => item.classList.toggle("active", item === button));
|
||||||
applyFusionDetail();
|
applyFusionDetail();
|
||||||
markDirty("display");
|
markDirty("display");
|
||||||
|
await loadFusion();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
document.querySelectorAll("[data-model-detail]").forEach((button) => {
|
document.querySelectorAll("[data-model-detail]").forEach((button) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user