feat: introduce a new layered spatial workspace architecture design, implement initial card positioning logic, and add container size observation for the layout system.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { constrainDimensions } from './utils/layoutUtils';
|
||||
import { constrainDimensions, getInitialPosition, CONTAINER_PADDING } from './utils/layoutUtils';
|
||||
import { fetchLayoutRecords, upsertLayoutRecords } from './db';
|
||||
|
||||
export interface LayoutCardState {
|
||||
@@ -23,18 +23,25 @@ export class LayoutCard implements LayoutCardState {
|
||||
|
||||
private _innerRadius: number = 0;
|
||||
private _outerRadius: number = 0;
|
||||
private _centerX: number = 0;
|
||||
private _centerY: number = 0;
|
||||
public store: LayoutStore;
|
||||
public dragStartX: number = 0;
|
||||
public dragStartY: number = 0;
|
||||
public isDirty: boolean = false;
|
||||
public intendedX: number = 0;
|
||||
public intendedY: number = 0;
|
||||
private dragOffset: { x: number, y: number } | 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.intendedX = this.x;
|
||||
this.intendedY = this.y;
|
||||
this.ref = ref;
|
||||
this.recalculateRadii();
|
||||
this.recalculateCenters();
|
||||
}
|
||||
|
||||
setRef(ref: HTMLElement | null) {
|
||||
@@ -48,11 +55,33 @@ export class LayoutCard implements LayoutCardState {
|
||||
this.dragStartY = this.y;
|
||||
}
|
||||
|
||||
getConstrainedPosition(x: number, y: number): { x: number, y: number } {
|
||||
const rad = (this.rotation * Math.PI) / 180;
|
||||
const sin = Math.abs(Math.sin(rad));
|
||||
const cos = Math.abs(Math.cos(rad));
|
||||
|
||||
const rotatedWidth = this.width * cos + this.height * sin;
|
||||
const rotatedHeight = this.width * sin + this.height * cos;
|
||||
|
||||
const minX = CONTAINER_PADDING + (rotatedWidth - this.width) / 2;
|
||||
const maxX = this.store.containerWidth - CONTAINER_PADDING - this.width - (rotatedWidth - this.width) / 2;
|
||||
|
||||
const minY = CONTAINER_PADDING + (rotatedHeight - this.height) / 2;
|
||||
const maxY = this.store.containerHeight - CONTAINER_PADDING - this.height - (rotatedHeight - this.height) / 2;
|
||||
|
||||
const newX = Math.max(minX, Math.min(x, maxX));
|
||||
const newY = Math.max(minY, Math.min(y, maxY));
|
||||
|
||||
return { x: newX, y: newY };
|
||||
}
|
||||
|
||||
continueDrag(delta: { x: number, y: number }) {
|
||||
if (!this.dragOffset) return;
|
||||
|
||||
const newX = this.x + delta.x;
|
||||
const newY = this.y + delta.y;
|
||||
const targetX = this.x + delta.x;
|
||||
const targetY = this.y + delta.y;
|
||||
|
||||
const { x: newX, y: newY } = this.getConstrainedPosition(targetX, targetY);
|
||||
|
||||
this.update({
|
||||
x: newX,
|
||||
@@ -64,12 +93,22 @@ export class LayoutCard implements LayoutCardState {
|
||||
this.dragOffset = null;
|
||||
}
|
||||
|
||||
update(changes: Partial<LayoutCardState>, options: { markDirty?: boolean } = {}) {
|
||||
update(changes: Partial<LayoutCardState>, options: { markDirty?: boolean, isConstraintUpdate?: boolean } = {}) {
|
||||
Object.assign(this, changes);
|
||||
|
||||
if (!options.isConstraintUpdate) {
|
||||
if (changes.x !== undefined) this.intendedX = changes.x;
|
||||
if (changes.y !== undefined) this.intendedY = changes.y;
|
||||
}
|
||||
|
||||
if (changes.width !== undefined || changes.height !== undefined) {
|
||||
this.recalculateRadii();
|
||||
}
|
||||
|
||||
if (changes.x !== undefined || changes.y !== undefined || changes.width !== undefined || changes.height !== undefined) {
|
||||
this.recalculateCenters();
|
||||
}
|
||||
|
||||
if (options.markDirty) {
|
||||
this.isDirty = true;
|
||||
}
|
||||
@@ -167,6 +206,11 @@ export class LayoutCard implements LayoutCardState {
|
||||
this._outerRadius = Math.sqrt(this.width * this.width + this.height * this.height) / 2;
|
||||
}
|
||||
|
||||
private recalculateCenters() {
|
||||
this._centerX = this.x + this.width / 2;
|
||||
this._centerY = this.y + this.height / 2;
|
||||
}
|
||||
|
||||
private rafId: number | null = null;
|
||||
|
||||
private applyTransform() {
|
||||
@@ -226,11 +270,21 @@ export class LayoutCard implements LayoutCardState {
|
||||
get outerRadius(): number {
|
||||
return this._outerRadius;
|
||||
}
|
||||
|
||||
get centerX(): number {
|
||||
return this._centerX;
|
||||
}
|
||||
|
||||
get centerY(): number {
|
||||
return this._centerY;
|
||||
}
|
||||
}
|
||||
|
||||
export class LayoutStore {
|
||||
items = new Map<string, LayoutCard>();
|
||||
zCounter = 100;
|
||||
containerWidth: number = 0;
|
||||
containerHeight: number = 0;
|
||||
|
||||
private savedLayouts = new Map<string, { x: number, y: number, rotation: number, z: number }>();
|
||||
private tenantId: string | null = null;
|
||||
@@ -263,26 +317,33 @@ export class LayoutStore {
|
||||
if (z >= this.zCounter) {
|
||||
this.zCounter = z + 1;
|
||||
}
|
||||
|
||||
card = new LayoutCard(id, this, {
|
||||
x,
|
||||
y,
|
||||
rotation,
|
||||
width,
|
||||
height,
|
||||
z
|
||||
}, ref);
|
||||
} else {
|
||||
// Apply defaults if not provided
|
||||
x = Math.random() * 500;
|
||||
y = Math.random() * 500;
|
||||
rotation = Math.random() * 10 - 5;
|
||||
z = this.zCounter++;
|
||||
}
|
||||
// Create card with temporary position
|
||||
card = new LayoutCard(id, this, {
|
||||
width,
|
||||
height,
|
||||
z: this.zCounter++
|
||||
}, ref);
|
||||
|
||||
card = new LayoutCard(id, this, {
|
||||
x,
|
||||
y,
|
||||
rotation,
|
||||
width,
|
||||
height,
|
||||
z
|
||||
}, ref);
|
||||
// Calculate initial position using the card instance
|
||||
const { x: initX, y: initY } = getInitialPosition(
|
||||
this.containerWidth,
|
||||
this.containerHeight,
|
||||
card,
|
||||
Array.from(this.items.values())
|
||||
);
|
||||
|
||||
// If it was a saved layout, it's not dirty.
|
||||
if (!saved) {
|
||||
card.isDirty = true;
|
||||
// Update card with calculated position
|
||||
card.update({ x: initX, y: initY }, { markDirty: true });
|
||||
}
|
||||
|
||||
this.items.set(id, card);
|
||||
@@ -302,6 +363,21 @@ export class LayoutStore {
|
||||
this.items.delete(id);
|
||||
}
|
||||
|
||||
setContainerSize(width: number, height: number) {
|
||||
this.containerWidth = width;
|
||||
this.containerHeight = height;
|
||||
this.relayout();
|
||||
}
|
||||
|
||||
relayout() {
|
||||
for (const card of this.items.values()) {
|
||||
const { x, y } = card.getConstrainedPosition(card.intendedX, card.intendedY);
|
||||
if (x !== card.x || y !== card.y) {
|
||||
card.update({ x, y }, { markDirty: false, isConstraintUpdate: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getCardsInCircle(x: number, y: number, radius: number): LayoutCard[] {
|
||||
const result: LayoutCard[] = [];
|
||||
for (const card of this.items.values()) {
|
||||
@@ -386,6 +462,9 @@ export class LayoutStore {
|
||||
card.update(saved, { markDirty: false });
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure everything is within bounds
|
||||
this.relayout();
|
||||
}
|
||||
|
||||
async saveLayout() {
|
||||
@@ -396,8 +475,8 @@ export class LayoutStore {
|
||||
|
||||
const entries = dirtyCards.map(card => ({
|
||||
documentId: card.id,
|
||||
centerX: card.x,
|
||||
centerY: card.y,
|
||||
centerX: card.intendedX,
|
||||
centerY: card.intendedY,
|
||||
rotation: card.rotation,
|
||||
zIndex: card.z,
|
||||
updatedAt: Date.now()
|
||||
|
||||
Reference in New Issue
Block a user