Merge branch 'dev'
This commit is contained in:
commit
9ae2298a24
|
|
@ -42,6 +42,9 @@ export class PresenceManager extends EventTarget {
|
|||
#fadeInterval: number | null = null;
|
||||
#localPeerId: string;
|
||||
#localUsername: string;
|
||||
#panX = 0;
|
||||
#panY = 0;
|
||||
#scale = 1;
|
||||
|
||||
constructor(container: HTMLElement, peerId: string, username?: string) {
|
||||
super();
|
||||
|
|
@ -68,6 +71,16 @@ export class PresenceManager extends EventTarget {
|
|||
this.#localUsername = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update camera transform so remote cursors render at correct screen positions
|
||||
*/
|
||||
setCamera(panX: number, panY: number, scale: number) {
|
||||
this.#panX = panX;
|
||||
this.#panY = panY;
|
||||
this.#scale = scale;
|
||||
this.#refreshCursors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update presence for a remote user
|
||||
*/
|
||||
|
|
@ -89,10 +102,12 @@ export class PresenceManager extends EventTarget {
|
|||
user.lastUpdate = Date.now();
|
||||
}
|
||||
|
||||
// Update cursor position
|
||||
// Update cursor position (world → screen conversion)
|
||||
if (data.cursor) {
|
||||
user.element.style.left = `${data.cursor.x}px`;
|
||||
user.element.style.top = `${data.cursor.y}px`;
|
||||
const screenX = data.cursor.x * this.#scale + this.#panX;
|
||||
const screenY = data.cursor.y * this.#scale + this.#panY;
|
||||
user.element.style.left = `${screenX}px`;
|
||||
user.element.style.top = `${screenY}px`;
|
||||
user.element.style.opacity = "1";
|
||||
}
|
||||
|
||||
|
|
@ -150,6 +165,17 @@ export class PresenceManager extends EventTarget {
|
|||
}
|
||||
}
|
||||
|
||||
#refreshCursors() {
|
||||
for (const [, user] of this.#users) {
|
||||
if (user.cursor) {
|
||||
const screenX = user.cursor.x * this.#scale + this.#panX;
|
||||
const screenY = user.cursor.y * this.#scale + this.#panY;
|
||||
user.element.style.left = `${screenX}px`;
|
||||
user.element.style.top = `${screenY}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ensureCursorContainer() {
|
||||
let cursorsEl = this.#container.querySelector(".presence-cursors") as HTMLElement;
|
||||
if (!cursorsEl) {
|
||||
|
|
|
|||
|
|
@ -1865,6 +1865,8 @@ const server = Bun.serve<WSData>({
|
|||
|
||||
loadCommunity(communitySlug).then((doc) => {
|
||||
if (!doc) return;
|
||||
// Guard: peer may have disconnected during async load
|
||||
if (ws.readyState !== WebSocket.OPEN) return;
|
||||
|
||||
if (mode === "json") {
|
||||
const docData = getDocumentData(communitySlug);
|
||||
|
|
|
|||
|
|
@ -1619,6 +1619,25 @@
|
|||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
#canvas-loading {
|
||||
position: fixed; inset: 0; z-index: 50;
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
gap: 1rem; pointer-events: none;
|
||||
}
|
||||
.canvas-loading__spinner {
|
||||
width: 36px; height: 36px;
|
||||
border: 3px solid rgba(99,102,241,0.2);
|
||||
border-top-color: #6366f1;
|
||||
border-radius: 50%;
|
||||
animation: canvas-loading-spin 0.8s linear infinite;
|
||||
}
|
||||
@keyframes canvas-loading-spin { to { transform: rotate(360deg); } }
|
||||
.canvas-loading__text {
|
||||
font-size: 0.85rem; color: #64748b;
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/shell.css">
|
||||
<style>
|
||||
|
|
@ -1952,6 +1971,10 @@
|
|||
</div>
|
||||
|
||||
<div id="canvas"><div id="canvas-content"></div></div>
|
||||
<div id="canvas-loading">
|
||||
<div class="canvas-loading__spinner"></div>
|
||||
<div class="canvas-loading__text">Loading canvas...</div>
|
||||
</div>
|
||||
<div id="select-rect"></div>
|
||||
|
||||
<script type="module">
|
||||
|
|
@ -2346,6 +2369,7 @@
|
|||
await offlineStore.open();
|
||||
const hadCache = await sync.initFromCache();
|
||||
if (hadCache) {
|
||||
document.getElementById("canvas-loading")?.remove();
|
||||
status.className = "offline";
|
||||
statusText.textContent = "Offline (cached)";
|
||||
}
|
||||
|
|
@ -2402,10 +2426,10 @@
|
|||
if (now - lastCursorUpdate < CURSOR_THROTTLE) return;
|
||||
lastCursorUpdate = now;
|
||||
|
||||
// Get cursor position relative to canvas
|
||||
// Convert screen → world coordinates (accounting for pan/zoom)
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
const x = (e.clientX - rect.left - panX) / scale;
|
||||
const y = (e.clientY - rect.top - panY) / scale;
|
||||
|
||||
// Send presence update via sync
|
||||
sync.sendPresence(presence.getLocalPresence({ x, y }, selectedShapeId));
|
||||
|
|
@ -2420,8 +2444,8 @@
|
|||
lastCursorUpdate = now;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = e.touches[0].clientX - rect.left;
|
||||
const y = e.touches[0].clientY - rect.top;
|
||||
const x = (e.touches[0].clientX - rect.left - panX) / scale;
|
||||
const y = (e.touches[0].clientY - rect.top - panY) / scale;
|
||||
|
||||
sync.sendPresence(presence.getLocalPresence({ x, y }, selectedShapeId));
|
||||
}, { passive: true });
|
||||
|
|
@ -2708,6 +2732,7 @@
|
|||
});
|
||||
|
||||
sync.addEventListener("synced", (e) => {
|
||||
document.getElementById("canvas-loading")?.remove();
|
||||
console.log("[Canvas] Initial sync complete:", e.detail.shapes);
|
||||
});
|
||||
|
||||
|
|
@ -4159,6 +4184,8 @@
|
|||
ghostEl.style.width = (defaults.width * scale) + "px";
|
||||
ghostEl.style.height = (defaults.height * scale) + "px";
|
||||
}
|
||||
// Update remote cursors to match new camera position
|
||||
presence.setCamera(panX, panY, scale);
|
||||
}
|
||||
|
||||
document.getElementById("zoom-in").addEventListener("click", () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue