From 98eaefa8014e175682f687426cce366680db4da4 Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Wed, 21 Oct 2026 08:00:03 +0800 Subject: [PATCH] fix: reset picking shapes before frame rebuilds --- src/GLModel.ts | 4 +++ src/intersectionShapes.d.ts | 7 +++++ src/intersectionShapes.js | 10 +++++++ tests/jest/intersectionShapes.test.js | 42 +++++++++++++++++++++++++++ 4 files changed, 63 insertions(+) create mode 100644 src/intersectionShapes.d.ts create mode 100644 src/intersectionShapes.js create mode 100644 tests/jest/intersectionShapes.test.js diff --git a/src/GLModel.ts b/src/GLModel.ts index 2a5d244e..311ed9b5 100644 --- a/src/GLModel.ts +++ b/src/GLModel.ts @@ -20,6 +20,7 @@ import { ArrowSpec } from "GLShape"; import { ParserOptionsSpec } from "./parsers/ParserOptionsSpec"; import { LabelSpec } from "Label"; import { assignBonds } from "./parsers/utils/assignBonds"; +import { resetIntersectionShapes } from "./intersectionShapes"; /** * GLModel represents a group of related atoms @@ -1666,6 +1667,9 @@ export class GLModel { for (let ai = 0; ai < atoms.length; ai++) this._coordIndex.set(atoms[ai], ai); this._coordAtomCount = atoms.length; + // Picking shapes are stored on atoms, so clear stale frame geometry before rebuilding. + resetIntersectionShapes(atoms); + var ret = new Object3D(); var cartoonAtoms = []; var lineGeometries: Record = {}; diff --git a/src/intersectionShapes.d.ts b/src/intersectionShapes.d.ts new file mode 100644 index 00000000..76ceb8ed --- /dev/null +++ b/src/intersectionShapes.d.ts @@ -0,0 +1,7 @@ +interface InteractiveAtom { + clickable?: boolean; + hoverable?: boolean; + intersectionShape?: any; +} + +export function resetIntersectionShapes(atoms: InteractiveAtom[]): void; diff --git a/src/intersectionShapes.js b/src/intersectionShapes.js new file mode 100644 index 00000000..127147f7 --- /dev/null +++ b/src/intersectionShapes.js @@ -0,0 +1,10 @@ +function resetIntersectionShapes(atoms) { + for (var i = 0; i < atoms.length; i++) { + var atom = atoms[i]; + if (atom && (atom.clickable || atom.hoverable)) { + atom.intersectionShape = { sphere: [], cylinder: [], line: [], triangle: [] }; + } + } +} + +module.exports = { resetIntersectionShapes: resetIntersectionShapes }; diff --git a/tests/jest/intersectionShapes.test.js b/tests/jest/intersectionShapes.test.js new file mode 100644 index 00000000..ff689628 --- /dev/null +++ b/tests/jest/intersectionShapes.test.js @@ -0,0 +1,42 @@ +const { resetIntersectionShapes } = require("../../src/intersectionShapes"); + +function oldShapes() { + return { sphere: ["stale"], cylinder: ["stale"], line: ["stale"], triangle: ["stale"] }; +} + +describe("picking-shape rebuilds", () => { + test("keeps clickable and hoverable frame shapes bounded on revisits", () => { + const frames = [ + [ + { clickable: true, intersectionShape: oldShapes() }, + { hoverable: true, intersectionShape: oldShapes() }, + { intersectionShape: { sphere: ["unchanged"] } }, + ], + [{ clickable: true, intersectionShape: oldShapes() }], + ]; + const noninteractiveShapes = frames[0][2].intersectionShape; + + for (let rebuild = 0; rebuild < 200; rebuild++) { + const frame = frames[rebuild % frames.length]; + resetIntersectionShapes(frame); + for (const atom of frame) { + if (atom.clickable || atom.hoverable) { + atom.intersectionShape.sphere.push(rebuild); + atom.intersectionShape.cylinder.push(rebuild); + } + } + } + + for (const frame of frames) { + for (const atom of frame) { + if (atom.clickable || atom.hoverable) { + expect(atom.intersectionShape.sphere).toHaveLength(1); + expect(atom.intersectionShape.cylinder).toHaveLength(1); + expect(atom.intersectionShape.line).toHaveLength(0); + expect(atom.intersectionShape.triangle).toHaveLength(0); + } + } + } + expect(frames[0][2].intersectionShape).toBe(noninteractiveShapes); + }); +});