Implement click-through cycling logic for object selection in viewport-host.ts
This commit is contained in:
@@ -1004,35 +1004,88 @@ export class ViewportHost {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (hits.length === 0) {
|
if (hits.length === 0) {
|
||||||
|
this.lastClickPointer = null;
|
||||||
|
this.lastClickSelectionKey = null;
|
||||||
this.brushSelectionChangeHandler?.({
|
this.brushSelectionChangeHandler?.({
|
||||||
kind: "none"
|
kind: "none"
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hit = hits[0];
|
// Build a deduplicated list of selectable candidates from the hit list.
|
||||||
const entityId = hit.object.userData.entityId;
|
// Multiple mesh parts of the same entity/model/brush collapse to one entry.
|
||||||
|
const candidates: Array<{ key: string; object: (typeof hits)[0]["object"]; face: (typeof hits)[0]["face"] }> = [];
|
||||||
|
const seenKeys = new Set<string>();
|
||||||
|
|
||||||
|
for (const hit of hits) {
|
||||||
|
const entityId = hit.object.userData.entityId;
|
||||||
|
const modelInstanceId = this.findModelInstanceId(hit.object);
|
||||||
|
const brushId = hit.object.userData.brushId;
|
||||||
|
|
||||||
|
let key: string;
|
||||||
|
if (typeof entityId === "string") {
|
||||||
|
key = `entity:${entityId}`;
|
||||||
|
} else if (modelInstanceId !== null) {
|
||||||
|
key = `model:${modelInstanceId}`;
|
||||||
|
} else if (typeof brushId === "string") {
|
||||||
|
const faceMaterialIndex = hit.face?.materialIndex;
|
||||||
|
const faceId = typeof faceMaterialIndex === "number" ? BOX_FACE_IDS[faceMaterialIndex] ?? null : null;
|
||||||
|
// In face-edit mode each face is a distinct candidate; in brush mode collapse to brush.
|
||||||
|
key = faceId !== null ? `brushFace:${brushId}:${faceId}` : `brush:${brushId}`;
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!seenKeys.has(key)) {
|
||||||
|
seenKeys.add(key);
|
||||||
|
candidates.push({ key, object: hit.object, face: hit.face });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidates.length === 0) {
|
||||||
|
this.lastClickPointer = null;
|
||||||
|
this.lastClickSelectionKey = null;
|
||||||
|
this.brushSelectionChangeHandler?.({ kind: "none" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine whether this click is at the same spot as the last one.
|
||||||
|
const POINTER_TOLERANCE = 0.01;
|
||||||
|
const isSameSpot =
|
||||||
|
this.lastClickPointer !== null &&
|
||||||
|
Math.abs(this.pointer.x - this.lastClickPointer.x) < POINTER_TOLERANCE &&
|
||||||
|
Math.abs(this.pointer.y - this.lastClickPointer.y) < POINTER_TOLERANCE;
|
||||||
|
|
||||||
|
let candidateIndex = 0;
|
||||||
|
|
||||||
|
if (isSameSpot && this.lastClickSelectionKey !== null) {
|
||||||
|
// Find where the previously selected item sits in the new hit list and advance by one.
|
||||||
|
const lastIndex = candidates.findIndex((c) => c.key === this.lastClickSelectionKey);
|
||||||
|
if (lastIndex !== -1) {
|
||||||
|
candidateIndex = (lastIndex + 1) % candidates.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastClickPointer = { x: this.pointer.x, y: this.pointer.y };
|
||||||
|
|
||||||
|
const chosen = candidates[candidateIndex];
|
||||||
|
this.lastClickSelectionKey = chosen.key;
|
||||||
|
|
||||||
|
// Dispatch the selection for the chosen candidate.
|
||||||
|
const entityId = chosen.object.userData.entityId;
|
||||||
if (typeof entityId === "string") {
|
if (typeof entityId === "string") {
|
||||||
this.brushSelectionChangeHandler?.({
|
this.brushSelectionChangeHandler?.({ kind: "entities", ids: [entityId] });
|
||||||
kind: "entities",
|
|
||||||
ids: [entityId]
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelInstanceId = this.findModelInstanceId(hit.object);
|
const modelInstanceId = this.findModelInstanceId(chosen.object);
|
||||||
|
|
||||||
if (modelInstanceId !== null) {
|
if (modelInstanceId !== null) {
|
||||||
this.brushSelectionChangeHandler?.({
|
this.brushSelectionChangeHandler?.({ kind: "modelInstances", ids: [modelInstanceId] });
|
||||||
kind: "modelInstances",
|
|
||||||
ids: [modelInstanceId]
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const brushId = hit.object.userData.brushId;
|
const brushId = chosen.object.userData.brushId;
|
||||||
const faceMaterialIndex = hit.face?.materialIndex;
|
const faceMaterialIndex = chosen.face?.materialIndex;
|
||||||
const faceId = typeof faceMaterialIndex === "number" ? BOX_FACE_IDS[faceMaterialIndex] ?? null : null;
|
const faceId = typeof faceMaterialIndex === "number" ? BOX_FACE_IDS[faceMaterialIndex] ?? null : null;
|
||||||
|
|
||||||
if (typeof brushId !== "string") {
|
if (typeof brushId !== "string") {
|
||||||
@@ -1040,18 +1093,11 @@ export class ViewportHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (faceId !== null) {
|
if (faceId !== null) {
|
||||||
this.brushSelectionChangeHandler?.({
|
this.brushSelectionChangeHandler?.({ kind: "brushFace", brushId, faceId });
|
||||||
kind: "brushFace",
|
|
||||||
brushId,
|
|
||||||
faceId
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.brushSelectionChangeHandler?.({
|
this.brushSelectionChangeHandler?.({ kind: "brushes", ids: [brushId] });
|
||||||
kind: "brushes",
|
|
||||||
ids: [brushId]
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private handlePointerMove = (event: PointerEvent) => {
|
private handlePointerMove = (event: PointerEvent) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user