UNPKG

@tldraw/editor

Version:

tldraw infinite canvas SDK (editor).

461 lines (460 loc) • 14.1 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var ScribbleManager_exports = {}; __export(ScribbleManager_exports, { ScribbleManager: () => ScribbleManager }); module.exports = __toCommonJS(ScribbleManager_exports); var import_utils = require("@tldraw/utils"); var import_Vec = require("../../../primitives/Vec"); class ScribbleManager { constructor(editor) { this.editor = editor; } editor; sessions = /* @__PURE__ */ new Map(); // ==================== SESSION API ==================== /** * Start a new session for grouping scribbles. * Returns a session ID that can be used with other session methods. * * @param options - Session configuration * @returns Session ID * @public */ startSession(options = {}) { const id = options.id ?? (0, import_utils.uniqueId)(); const session = { id, items: [], state: "active", options: { selfConsume: options.selfConsume ?? true, idleTimeoutMs: options.idleTimeoutMs ?? 0, fadeMode: options.fadeMode ?? "individual", fadeEasing: options.fadeEasing ?? (options.fadeMode === "grouped" ? "ease-in" : "linear"), fadeDurationMs: options.fadeDurationMs ?? this.editor.options.laserFadeoutMs }, fadeElapsed: 0, totalPointsAtFadeStart: 0 }; this.sessions.set(id, session); if (session.options.idleTimeoutMs > 0) { this.resetIdleTimeout(session); } return id; } /** * Add a scribble to a session. * * @param sessionId - The session ID * @param scribble - Partial scribble properties * @param scribbleId - Optional scribble ID * @public */ addScribbleToSession(sessionId, scribble, scribbleId = (0, import_utils.uniqueId)()) { const session = this.sessions.get(sessionId); if (!session) throw Error(`Session ${sessionId} not found`); const item = { id: scribbleId, scribble: { id: scribbleId, size: 20, color: "accent", opacity: 0.8, delay: 0, points: [], shrink: 0.1, taper: true, ...scribble, state: "starting" }, timeoutMs: 0, delayRemaining: scribble.delay ?? 0, prev: null, next: null }; session.items.push(item); if (session.options.idleTimeoutMs > 0) { this.resetIdleTimeout(session); } return item; } /** * Add a point to a scribble in a session. * * @param sessionId - The session ID * @param scribbleId - The scribble ID * @param x - X coordinate * @param y - Y coordinate * @param z - Z coordinate (pressure) * @public */ addPointToSession(sessionId, scribbleId, x, y, z = 0.5) { const session = this.sessions.get(sessionId); if (!session) throw Error(`Session ${sessionId} not found`); const item = session.items.find((i) => i.id === scribbleId); if (!item) throw Error(`Scribble ${scribbleId} not found in session ${sessionId}`); const point = { x, y, z }; if (!item.prev || import_Vec.Vec.Dist(item.prev, point) >= 1) { item.next = point; } if (session.options.idleTimeoutMs > 0) { this.resetIdleTimeout(session); } return item; } /** * Extend a session, resetting its idle timeout. * * @param sessionId - The session ID * @public */ extendSession(sessionId) { const session = this.sessions.get(sessionId); if (!session) return; if (session.options.idleTimeoutMs > 0) { this.resetIdleTimeout(session); } } /** * Stop a session, triggering fade-out. * * @param sessionId - The session ID * @public */ stopSession(sessionId) { const session = this.sessions.get(sessionId); if (!session || session.state !== "active") return; this.clearIdleTimeout(session); session.state = "stopping"; if (session.options.fadeMode === "grouped") { session.totalPointsAtFadeStart = session.items.reduce( (sum, item) => sum + item.scribble.points.length, 0 ); session.fadeElapsed = 0; for (const item of session.items) { item.scribble.state = "stopping"; } } else { for (const item of session.items) { item.delayRemaining = Math.min(item.delayRemaining, 200); item.scribble.state = "stopping"; } } } /** * Clear all scribbles in a session immediately. * * @param sessionId - The session ID * @public */ clearSession(sessionId) { const session = this.sessions.get(sessionId); if (!session) return; this.clearIdleTimeout(session); for (const item of session.items) { item.scribble.points.length = 0; } session.state = "complete"; } /** * Check if a session is active. * * @param sessionId - The session ID * @public */ isSessionActive(sessionId) { const session = this.sessions.get(sessionId); return session?.state === "active"; } // ==================== SIMPLE API (for eraser, select, etc.) ==================== /** * Add a scribble using the default self-consuming behavior. * Creates an implicit session for the scribble. * * @param scribble - Partial scribble properties * @param id - Optional scribble id * @returns The created scribble item * @public */ addScribble(scribble, id = (0, import_utils.uniqueId)()) { const sessionId = this.startSession(); return this.addScribbleToSession(sessionId, scribble, id); } /** * Add a point to a scribble. Searches all sessions. * * @param id - The scribble id * @param x - X coordinate * @param y - Y coordinate * @param z - Z coordinate (pressure) * @public */ addPoint(id, x, y, z = 0.5) { for (const session of this.sessions.values()) { const item = session.items.find((i) => i.id === id); if (item) { const point = { x, y, z }; if (!item.prev || import_Vec.Vec.Dist(item.prev, point) >= 1) { item.next = point; } if (session.options.idleTimeoutMs > 0) { this.resetIdleTimeout(session); } return item; } } throw Error(`Scribble with id ${id} not found`); } /** * Mark a scribble as complete (done being drawn but not yet fading). * Searches all sessions. * * @param id - The scribble id * @public */ complete(id) { for (const session of this.sessions.values()) { const item = session.items.find((i) => i.id === id); if (item) { if (item.scribble.state === "starting" || item.scribble.state === "active") { item.scribble.state = "complete"; } return item; } } throw Error(`Scribble with id ${id} not found`); } /** * Stop a scribble. Searches all sessions. * * @param id - The scribble id * @public */ stop(id) { for (const session of this.sessions.values()) { const item = session.items.find((i) => i.id === id); if (item) { item.delayRemaining = Math.min(item.delayRemaining, 200); item.scribble.state = "stopping"; return item; } } throw Error(`Scribble with id ${id} not found`); } /** * Stop and remove all sessions. * * @public */ reset() { for (const session of this.sessions.values()) { this.clearIdleTimeout(session); } this.sessions.clear(); this.editor.updateInstanceState({ scribbles: [] }); } /** * Update on each animation frame. * * @param elapsed - The number of milliseconds since the last tick. * @public */ tick(elapsed) { const currentScribbles = this.editor.getInstanceState().scribbles; if (this.sessions.size === 0 && currentScribbles.length === 0) return; this.editor.run(() => { for (const session of this.sessions.values()) { this.tickSession(session, elapsed); } for (const [id, session] of this.sessions) { if (session.state === "complete") { this.clearIdleTimeout(session); this.sessions.delete(id); } } const scribbles = []; for (const session of this.sessions.values()) { for (const item of session.items) { if (item.scribble.points.length > 0) { scribbles.push({ ...item.scribble, points: [...item.scribble.points] }); } } } this.editor.updateInstanceState({ scribbles }); }); } // ==================== PRIVATE HELPERS ==================== resetIdleTimeout(session) { this.clearIdleTimeout(session); session.idleTimeoutHandle = this.editor.timers.setTimeout(() => { this.stopSession(session.id); }, session.options.idleTimeoutMs); } clearIdleTimeout(session) { if (session.idleTimeoutHandle !== void 0) { clearTimeout(session.idleTimeoutHandle); session.idleTimeoutHandle = void 0; } } tickSession(session, elapsed) { if (session.state === "complete") return; if (session.state === "stopping" && session.options.fadeMode === "grouped") { this.tickGroupedFade(session, elapsed); } else { this.tickSessionItems(session, elapsed); } const hasContent = session.items.some((item) => item.scribble.points.length > 0); if (!hasContent && (session.state === "stopping" || session.items.length === 0)) { session.state = "complete"; } } tickSessionItems(session, elapsed) { for (const item of session.items) { const shouldSelfConsume = session.options.selfConsume || session.state === "stopping" || item.scribble.state === "stopping"; if (shouldSelfConsume) { this.tickSelfConsumingItem(item, elapsed); } else { this.tickPersistentItem(item); } } if (session.options.fadeMode === "individual") { for (let i = session.items.length - 1; i >= 0; i--) { if (session.items[i].scribble.points.length === 0) { session.items.splice(i, 1); } } } } tickPersistentItem(item) { const { scribble } = item; if (scribble.state === "starting") { const { next, prev } = item; if (next && next !== prev) { item.prev = next; scribble.points.push(next); } if (scribble.points.length > 8) { scribble.state = "active"; } return; } if (scribble.state === "active") { const { next, prev } = item; if (next && next !== prev) { item.prev = next; scribble.points.push(next); } } } tickSelfConsumingItem(item, elapsed) { const { scribble } = item; if (scribble.state === "starting") { const { next: next2, prev: prev2 } = item; if (next2 && next2 !== prev2) { item.prev = next2; scribble.points.push(next2); } if (scribble.points.length > 8) { scribble.state = "active"; } return; } if (item.delayRemaining > 0) { item.delayRemaining = Math.max(0, item.delayRemaining - elapsed); } item.timeoutMs += elapsed; if (item.timeoutMs >= 16) { item.timeoutMs = 0; } const { delayRemaining, timeoutMs, prev, next } = item; switch (scribble.state) { case "active": { if (next && next !== prev) { item.prev = next; scribble.points.push(next); if (delayRemaining === 0 && scribble.points.length > 8) { scribble.points.shift(); } } else { if (timeoutMs === 0) { if (scribble.points.length > 1) { scribble.points.shift(); } else { item.delayRemaining = scribble.delay; } } } break; } case "stopping": { if (delayRemaining === 0 && timeoutMs === 0) { if (scribble.points.length <= 1) { scribble.points.length = 0; return; } if (scribble.shrink) { scribble.size = Math.max(1, scribble.size * (1 - scribble.shrink)); } scribble.points.shift(); } break; } case "paused": { break; } } } tickGroupedFade(session, elapsed) { session.fadeElapsed += elapsed; let remainingPoints = 0; for (const item of session.items) { remainingPoints += item.scribble.points.length; } if (remainingPoints === 0) return; if (session.fadeElapsed >= session.options.fadeDurationMs) { for (const item of session.items) { item.scribble.points.length = 0; } return; } const progress = session.fadeElapsed / session.options.fadeDurationMs; const easedProgress = session.options.fadeEasing === "ease-in" ? progress * progress : progress; const targetRemoved = Math.floor(easedProgress * session.totalPointsAtFadeStart); const actuallyRemoved = session.totalPointsAtFadeStart - remainingPoints; const pointsToRemove = Math.max(1, targetRemoved - actuallyRemoved); let removed = 0; let itemIndex = 0; while (removed < pointsToRemove && itemIndex < session.items.length) { const item = session.items[itemIndex]; if (item.scribble.points.length > 0) { item.scribble.points.shift(); removed++; } else { itemIndex++; } } } } //# sourceMappingURL=ScribbleManager.js.map