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:
2025-11-26 21:54:11 +01:00
parent ee1c5dc606
commit e36732cc2d
3 changed files with 189 additions and 58 deletions
+102 -23
View File
@@ -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()