refactor: Introduce a layered architecture for card interactions, separating drag logic, and selection handling.
This commit is contained in:
@@ -22,9 +22,13 @@ export class LayoutCard implements LayoutCardState {
|
||||
|
||||
private _innerRadius: number = 0;
|
||||
private _outerRadius: number = 0;
|
||||
public store: LayoutStore;
|
||||
public dragStartX: number = 0;
|
||||
public dragStartY: number = 0;
|
||||
|
||||
constructor(id: string, initialData: Partial<LayoutCardState> = {}, ref: HTMLElement | null = null) {
|
||||
constructor(id: string, store: LayoutStore, initialData: Partial<LayoutCardState> = {}, ref: HTMLElement | null = null) {
|
||||
this.id = id;
|
||||
this.store = store;
|
||||
Object.assign(this, initialData);
|
||||
this.ref = ref;
|
||||
this.recalculateRadii();
|
||||
@@ -35,6 +39,11 @@ export class LayoutCard implements LayoutCardState {
|
||||
this.applyTransform();
|
||||
}
|
||||
|
||||
captureDragStart() {
|
||||
this.dragStartX = this.x;
|
||||
this.dragStartY = this.y;
|
||||
}
|
||||
|
||||
update(changes: Partial<LayoutCardState>) {
|
||||
Object.assign(this, changes);
|
||||
if (changes.width !== undefined || changes.height !== undefined) {
|
||||
@@ -43,19 +52,113 @@ export class LayoutCard implements LayoutCardState {
|
||||
this.applyTransform();
|
||||
}
|
||||
|
||||
isUnobstructed(): boolean {
|
||||
for (const other of this.store.items.values()) {
|
||||
if (other.id === this.id) continue;
|
||||
if (other.z <= this.z) continue;
|
||||
|
||||
const dx = other.x - this.x;
|
||||
const dy = other.y - this.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// Broad phase: check outer radii
|
||||
if (distance < this.outerRadius + other.outerRadius) {
|
||||
// Narrow phase: SAT intersection test
|
||||
if (this.intersects(other)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bringToFront() {
|
||||
this.z = this.store.zCounter++;
|
||||
}
|
||||
|
||||
private getVertices(): { x: number; y: number }[] {
|
||||
const rad = (this.rotation * Math.PI) / 180;
|
||||
const cos = Math.cos(rad);
|
||||
const sin = Math.sin(rad);
|
||||
const hw = this.width / 2;
|
||||
const hh = this.height / 2;
|
||||
|
||||
// Corners relative to center, then rotated, then translated
|
||||
// (-hw, -hh), (hw, -hh), (hw, hh), (-hw, hh)
|
||||
const corners = [
|
||||
{ x: -hw, y: -hh },
|
||||
{ x: hw, y: -hh },
|
||||
{ x: hw, y: hh },
|
||||
{ x: -hw, y: hh }
|
||||
];
|
||||
|
||||
return corners.map(p => ({
|
||||
x: (p.x * cos - p.y * sin) + this.x,
|
||||
y: (p.x * sin + p.y * cos) + this.y
|
||||
}));
|
||||
}
|
||||
|
||||
private getAxes(): { x: number; y: number }[] {
|
||||
const rad = (this.rotation * Math.PI) / 180;
|
||||
const cos = Math.cos(rad);
|
||||
const sin = Math.sin(rad);
|
||||
// Normals of the edges (local x and y axes)
|
||||
return [
|
||||
{ x: cos, y: sin },
|
||||
{ x: -sin, y: cos }
|
||||
];
|
||||
}
|
||||
|
||||
private intersects(other: LayoutCard): boolean {
|
||||
const verticesA = this.getVertices();
|
||||
const verticesB = other.getVertices();
|
||||
const axes = [...this.getAxes(), ...other.getAxes()];
|
||||
|
||||
for (const axis of axes) {
|
||||
const pA = this.project(verticesA, axis);
|
||||
const pB = this.project(verticesB, axis);
|
||||
|
||||
if (pA.max < pB.min || pB.max < pA.min) {
|
||||
return false; // Gap found, no intersection
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private project(vertices: { x: number; y: number }[], axis: { x: number; y: number }) {
|
||||
let min = Infinity;
|
||||
let max = -Infinity;
|
||||
for (const v of vertices) {
|
||||
const dot = v.x * axis.x + v.y * axis.y;
|
||||
if (dot < min) min = dot;
|
||||
if (dot > max) max = dot;
|
||||
}
|
||||
return { min, max };
|
||||
}
|
||||
|
||||
private recalculateRadii() {
|
||||
this._innerRadius = Math.min(this.width, this.height) / 2;
|
||||
this._outerRadius = Math.sqrt(this.width * this.width + this.height * this.height) / 2;
|
||||
}
|
||||
|
||||
private rafId: number | null = null;
|
||||
|
||||
private applyTransform() {
|
||||
if (this.ref) {
|
||||
this.ref.style.transform =
|
||||
`translate3d(${this.x}px, ${this.y}px, 0) rotate(${this.rotation}deg)`;
|
||||
this.ref.style.zIndex = String(this.z);
|
||||
this.ref.style.width = `${this.width}px`;
|
||||
this.ref.style.height = `${this.height}px`;
|
||||
if (this.rafId) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
}
|
||||
|
||||
this.rafId = requestAnimationFrame(() => {
|
||||
if (this.ref) {
|
||||
this.ref.style.transform =
|
||||
`translate3d(${this.x}px, ${this.y}px, 0) rotate(${this.rotation}deg)`;
|
||||
this.ref.style.zIndex = String(this.z);
|
||||
this.ref.style.width = `${this.width}px`;
|
||||
this.ref.style.height = `${this.height}px`;
|
||||
}
|
||||
this.rafId = null;
|
||||
});
|
||||
}
|
||||
|
||||
toSnapshot(): LayoutCardState {
|
||||
@@ -101,12 +204,13 @@ export class LayoutStore {
|
||||
const y = Math.random() * 500;
|
||||
const rotation = Math.random() * 10 - 5;
|
||||
|
||||
card = new LayoutCard(id, {
|
||||
card = new LayoutCard(id, this, {
|
||||
x,
|
||||
y,
|
||||
rotation,
|
||||
width,
|
||||
height
|
||||
height,
|
||||
z: this.zCounter++
|
||||
}, ref);
|
||||
this.items.set(id, card);
|
||||
} else {
|
||||
@@ -125,18 +229,6 @@ export class LayoutStore {
|
||||
this.items.delete(id);
|
||||
}
|
||||
|
||||
update(id: string, updates: Partial<LayoutCardState>) {
|
||||
const card = this.items.get(id);
|
||||
if (card) {
|
||||
if (updates.z) this.zCounter = Math.max(this.zCounter, updates.z);
|
||||
card.update(updates);
|
||||
}
|
||||
}
|
||||
|
||||
bringToFront(id: string) {
|
||||
this.update(id, { z: ++this.zCounter });
|
||||
}
|
||||
|
||||
getCardsInCircle(x: number, y: number, radius: number): LayoutCard[] {
|
||||
const result: LayoutCard[] = [];
|
||||
for (const card of this.items.values()) {
|
||||
|
||||
Reference in New Issue
Block a user