feat: Introduce new LayoutSystem and spatial workspace architecture design document, refactor document card and workspace to use it, and simplify pointer event handling.

This commit is contained in:
2025-11-26 13:39:19 +01:00
parent 27eea08605
commit bbf7d23c96
6 changed files with 167 additions and 113 deletions
+89 -62
View File
@@ -1,4 +1,6 @@
export interface LayoutItem {
import { constrainDimensions } from './utils/layoutUtils';
export interface LayoutCardState {
id: string;
x: number;
y: number;
@@ -6,79 +8,108 @@ export interface LayoutItem {
rotation: number;
width: number;
height: number;
ref: HTMLElement;
}
export class LayoutCard implements LayoutCardState {
id: string;
x: number = 0;
y: number = 0;
z: number = 0;
rotation: number = 0;
width: number = 0;
height: number = 0;
ref: HTMLElement | null = null;
constructor(id: string, initialData: Partial<LayoutCardState> = {}, ref: HTMLElement | null = null) {
this.id = id;
Object.assign(this, initialData);
this.ref = ref;
}
setRef(ref: HTMLElement | null) {
this.ref = ref;
this.applyTransform();
}
update(changes: Partial<LayoutCardState>) {
Object.assign(this, changes);
this.applyTransform();
}
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`;
}
}
toSnapshot(): LayoutCardState {
return {
id: this.id,
x: this.x,
y: this.y,
z: this.z,
rotation: this.rotation,
width: this.width,
height: this.height
};
}
}
export class LayoutStore {
items = new Map<string, LayoutItem>();
items = new Map<string, LayoutCard>();
zCounter = 100;
register(id: string, ref: HTMLElement, initialData: Partial<LayoutItem>) {
const existing = this.items.get(id);
this.items.set(id, {
id,
ref,
x: existing?.x ?? 0,
y: existing?.y ?? 0,
z: existing?.z ?? 0,
rotation: existing?.rotation ?? 0,
width: 200,
height: 200,
...initialData
});
}
initialize(id: string, ref: HTMLElement, config: {
x?: number;
y?: number;
rotation?: number;
z: number;
initialize(id: string, ref: HTMLElement | null, config: {
width: number;
height: number;
}) {
if (this.items.has(id)) {
// Update ref if it changed
const item = this.items.get(id)!;
if (item.ref !== ref) {
item.ref = ref;
this.update(id, {}); // Re-apply styles
}
return;
let card = this.items.get(id);
const { width, height } = constrainDimensions(
config.width,
config.height,
280
);
if (!card) {
// Apply defaults if not provided
const x = Math.random() * 500;
const y = Math.random() * 500;
const rotation = Math.random() * 10 - 5;
card = new LayoutCard(id, {
x,
y,
rotation,
width,
height
}, ref);
this.items.set(id, card);
} else {
card.update({ width, height });
}
// Apply defaults if not provided
const x = config.x ?? Math.random() * 500;
const y = config.y ?? Math.random() * 500;
const rotation = config.rotation ?? (Math.random() * 10 - 5);
// Always update ref and ensure transform is applied
if (card.ref !== ref) {
card.setRef(ref);
}
this.register(id, ref, {
...config,
x,
y,
rotation
});
// Apply immediately
this.update(id, {});
return card;
}
unregister(id: string) {
this.items.delete(id);
}
// Fast Update: Updates internal state AND applies CSS transform immediately
update(id: string, updates: Partial<LayoutItem>) {
const item = this.items.get(id);
if (!item) return;
Object.assign(item, updates);
if (updates.z) this.zCounter = Math.max(this.zCounter, updates.z);
// Direct DOM manipulation (The "Engine" part)
if (item.ref) {
item.ref.style.transform =
`translate3d(${item.x}px, ${item.y}px, 0) rotate(${item.rotation}deg)`;
item.ref.style.zIndex = String(item.z);
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);
}
}
@@ -87,10 +118,6 @@ export class LayoutStore {
}
getSnapshot() {
// Return serializable data for persistence
return Array.from(this.items.values()).map(({ ref: _ref, ...data }) => data);
return Array.from(this.items.values()).map(card => card.toSnapshot());
}
}
// Singleton or Context-provided instance
export const globalLayout = new LayoutStore();