From 404fcbfe39bab48434bcbc96685469d0892c7855 Mon Sep 17 00:00:00 2001 From: Max Richter Date: Wed, 10 Apr 2024 14:27:23 +0200 Subject: [PATCH] feat: make graph work in div --- app/package.json | 2 +- app/src/lib/components/graph/Graph.svelte | 133 ++-- app/src/lib/components/graph/Wrapper.svelte | 10 + app/src/lib/components/graph/index.ts | 2 + app/src/lib/graph-manager.ts | 2 +- app/src/lib/grid/Cell.svelte | 65 ++ app/src/lib/grid/Grid.svelte | 9 + app/src/lib/grid/Row.svelte | 33 + app/src/lib/grid/index.ts | 6 + app/src/lib/helpers/index.ts | 11 + app/src/lib/helpers/localStore.ts | 53 ++ app/src/lib/node-registry.ts | 2 +- app/src/lib/panzoom/domController.ts | 52 -- app/src/lib/panzoom/index.ts | 773 -------------------- app/src/lib/panzoom/kinetic.ts | 146 ---- app/src/routes/+page.svelte | 41 +- app/src/routes/app.css | 4 + app/svelte.config.js | 18 +- app/vite.config.ts | 5 + package.json | 3 +- packages/node-registry/svelte.config.js | 20 +- packages/node-registry/vite.config.ts | 4 + 22 files changed, 331 insertions(+), 1063 deletions(-) create mode 100644 app/src/lib/components/graph/Wrapper.svelte create mode 100644 app/src/lib/grid/Cell.svelte create mode 100644 app/src/lib/grid/Grid.svelte create mode 100644 app/src/lib/grid/Row.svelte create mode 100644 app/src/lib/grid/index.ts create mode 100644 app/src/lib/helpers/localStore.ts delete mode 100644 app/src/lib/panzoom/domController.ts delete mode 100644 app/src/lib/panzoom/index.ts delete mode 100644 app/src/lib/panzoom/kinetic.ts diff --git a/app/package.json b/app/package.json index 32ffc4f..3724627 100644 --- a/app/package.json +++ b/app/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "dev": "vite --host", "build": "vite build", "preview": "vite preview", "tauri:dev": "tauri dev", diff --git a/app/src/lib/components/graph/Graph.svelte b/app/src/lib/components/graph/Graph.svelte index dd36812..ce03f7d 100644 --- a/app/src/lib/components/graph/Graph.svelte +++ b/app/src/lib/components/graph/Graph.svelte @@ -1,5 +1,7 @@ - - - + bind:this={wrapper} + class="wrapper" + aria-label="Graph" + role="button" + tabindex="0" + bind:clientWidth={width} + bind:clientHeight={height} + on:keydown={handleKeyDown} + on:mousemove={handleMouseMove} + on:mousedown={handleMouseDown} + on:mouseup={handleMouseUp} +> + + - + - + {#if boxSelection && mouseDown} + + {/if} -{#if boxSelection && mouseDown} - -{/if} + {#if $status === "idle"} + {#if addMenuPosition} + + {/if} -{#if $status === "idle"} - {#if addMenuPosition} - - {/if} + {#if $activeSocket} + + {/if} - {#if $activeSocket} - - {/if} + + {:else if $status === "loading"} + Loading + {:else if $status === "error"} + Error + {/if} + + - -{:else if $status === "loading"} - Loading -{:else if $status === "error"} - Error -{/if} + diff --git a/app/src/lib/components/graph/Wrapper.svelte b/app/src/lib/components/graph/Wrapper.svelte new file mode 100644 index 0000000..0eca272 --- /dev/null +++ b/app/src/lib/components/graph/Wrapper.svelte @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/lib/components/graph/index.ts b/app/src/lib/components/graph/index.ts index e69de29..e6f3e44 100644 --- a/app/src/lib/components/graph/index.ts +++ b/app/src/lib/components/graph/index.ts @@ -0,0 +1,2 @@ +import Wrapper from './Wrapper.svelte'; +export default Wrapper; diff --git a/app/src/lib/graph-manager.ts b/app/src/lib/graph-manager.ts index 05d6f75..f918560 100644 --- a/app/src/lib/graph-manager.ts +++ b/app/src/lib/graph-manager.ts @@ -1,5 +1,5 @@ import { writable, type Writable } from "svelte/store"; -import type { Graph, Node, Edge, Socket, NodeRegistry, RuntimeExecutor, } from "@nodes/s"; +import type { Graph, Node, Edge, Socket, NodeRegistry, RuntimeExecutor, } from "@nodes/types"; import { HistoryManager } from "./history-manager"; import * as templates from "./graphs"; import EventEmitter from "./helpers/EventEmitter"; diff --git a/app/src/lib/grid/Cell.svelte b/app/src/lib/grid/Cell.svelte new file mode 100644 index 0000000..2aa66ac --- /dev/null +++ b/app/src/lib/grid/Cell.svelte @@ -0,0 +1,65 @@ + + + (mouseDown = false)} + on:mousemove={handleMouseMove} +/> + +{#if index > 0} +
+{/if} + +
+ +
+ + diff --git a/app/src/lib/grid/Grid.svelte b/app/src/lib/grid/Grid.svelte new file mode 100644 index 0000000..4db7b1b --- /dev/null +++ b/app/src/lib/grid/Grid.svelte @@ -0,0 +1,9 @@ + + + diff --git a/app/src/lib/grid/Row.svelte b/app/src/lib/grid/Row.svelte new file mode 100644 index 0000000..83f1399 --- /dev/null +++ b/app/src/lib/grid/Row.svelte @@ -0,0 +1,33 @@ + + +
+ +
+ + diff --git a/app/src/lib/grid/index.ts b/app/src/lib/grid/index.ts new file mode 100644 index 0000000..77eb2f5 --- /dev/null +++ b/app/src/lib/grid/index.ts @@ -0,0 +1,6 @@ +import { withSubComponents } from "$lib/helpers"; +import Grid from "./Grid.svelte"; +import Row from "./Row.svelte"; +import Cell from "./Cell.svelte"; + +export default withSubComponents(Grid, { Row, Cell }); diff --git a/app/src/lib/helpers/index.ts b/app/src/lib/helpers/index.ts index 57b9882..9d60856 100644 --- a/app/src/lib/helpers/index.ts +++ b/app/src/lib/helpers/index.ts @@ -94,3 +94,14 @@ export const createLogger = (() => { } })(); + +export function withSubComponents>( + component: A, + subcomponents: B +): A & B { + Object.keys(subcomponents).forEach((key) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (component as any)[key] = (subcomponents as any)[key]; + }); + return component as A & B; +} diff --git a/app/src/lib/helpers/localStore.ts b/app/src/lib/helpers/localStore.ts new file mode 100644 index 0000000..7b9e2ad --- /dev/null +++ b/app/src/lib/helpers/localStore.ts @@ -0,0 +1,53 @@ +import { writable, type Writable } from "svelte/store"; + +function isStore(v: unknown): v is Writable { + return v !== null && typeof v === "object" && "subscribe" in v && "set" in v; +} + +const storeIds: Map> = new Map(); + +const HAS_LOCALSTORAGE = "localStorage" in globalThis; + +function createLocalStore(key: string, initialValue: T | Writable) { + + let store: Writable; + + if (HAS_LOCALSTORAGE) { + const localValue = localStorage.getItem(key); + const value = localValue ? JSON.parse(localValue) : null; + if (value === null) { + if (isStore(initialValue)) { + store = initialValue; + } else { + store = writable(initialValue); + } + } else { + store = writable(value); + } + } else { + return isStore(initialValue) ? initialValue : writable(initialValue); + } + + store.subscribe((value) => { + localStorage.setItem(key, JSON.stringify(value)); + }); + + return { + subscribe: store.subscribe, + set: store.set, + update: store.update + } +} + + +export default function localStore(key: string, initialValue: T | Writable): Writable { + + if (storeIds.has(key)) return storeIds.get(key) as Writable; + + const store = createLocalStore(key, initialValue) + + storeIds.set(key, store); + + return store + +} diff --git a/app/src/lib/node-registry.ts b/app/src/lib/node-registry.ts index 32752a1..3c9e787 100644 --- a/app/src/lib/node-registry.ts +++ b/app/src/lib/node-registry.ts @@ -14,7 +14,7 @@ const nodeTypes: NodeType[] = [ { id: "max/plantarium/math", inputs: { - "op_type": { title: "type", type: "select", labels: ["add", "subtract", "multiply", "divide"], value: 0 }, + "op_type": { label: "type", type: "select", labels: ["add", "subtract", "multiply", "divide"], value: 0 }, "a": { type: "float" }, "b": { type: "float" }, }, diff --git a/app/src/lib/panzoom/domController.ts b/app/src/lib/panzoom/domController.ts deleted file mode 100644 index 06fb70f..0000000 --- a/app/src/lib/panzoom/domController.ts +++ /dev/null @@ -1,52 +0,0 @@ -export default function makeDomController(domElement: HTMLElement) { - const elementValid = isDomElement(domElement); - if (!elementValid) { - throw new Error( - 'panzoom requires DOM element to be attached to the DOM tree', - ); - } - - const owner = domElement.parentElement; - domElement.scrollTop = 0; - - const api = { - getBBox: getBBox, - getOwner: getOwner, - applyTransform: applyTransform, - }; - - return api; - - function getOwner() { - return owner; - } - - function getBBox() { - // TODO: We should probably cache this? - return { - left: 0, - top: 0, - width: domElement.clientWidth, - height: domElement.clientHeight, - }; - } - - function applyTransform(transform: { scale: number; x: number; y: number }) { - // TODO: Should we cache this? - domElement.style.transformOrigin = '0 0 0'; - domElement.style.transform = - 'matrix(' + - transform.scale + - ', 0, 0, ' + - transform.scale + - ', ' + - transform.x + - ', ' + - transform.y + - ')'; - } -} - -export function isDomElement(element: HTMLElement) { - return element && element.parentElement && element.style; -} diff --git a/app/src/lib/panzoom/index.ts b/app/src/lib/panzoom/index.ts deleted file mode 100644 index e5c2f37..0000000 --- a/app/src/lib/panzoom/index.ts +++ /dev/null @@ -1,773 +0,0 @@ -import type NodeSystemView from '../../view/NodeSystemView'; -import makeDomController from './domController'; -import kinetic from './kinetic'; - -interface Bounds { - left: number; - top: number; - right: number; - bottom: number; -} -export interface Transform { - x: number; - y: number; - scale: number; -} - -export interface TransformOrigin { - x: number; - y: number; -} - -export interface PanZoomController { - getOwner: () => Element; - applyTransform: (transform: Transform) => void; -} - -interface PanZoomOptions { - filterKey?: () => boolean; - bounds?: boolean | Bounds; - maxZoom?: number; - minZoom?: number; - boundsPadding?: number; - zoomDoubleClickSpeed?: number; - zoomSpeed?: number; - initialX?: number; - initialY?: number; - initialZoom?: number; - pinchSpeed?: number; - beforeWheel?: (e: WheelEvent) => void; - beforeMouseDown?: (e: MouseEvent) => void; - autocenter?: boolean; - onTouch?: (e: TouchEvent) => void; - onTransform?: (t: Transform) => void; - onDoubleClick?: (e: Event) => void; - smoothScroll?: Record; - controller?: PanZoomController; - enableTextSelection?: boolean; - disableKeyboardInteraction?: boolean; - transformOrigin?: TransformOrigin; - view?: NodeSystemView; -} - -const defaultZoomSpeed = 0.2; - -/** - * Creates a new instance of panzoom, so that an object can be panned and zoomed - * - * @param {DOMElement} domElement where panzoom should be attached. - * @param {Object} options that configure behavior. - */ -export function createPanZoom( - domElement: HTMLElement, - options: PanZoomOptions, -) { - const panController = makeDomController(domElement); - - const owner = panController.getOwner(); - // just to avoid GC pressure, every time we do intermediate transform - // we return this object. For internal use only. Never give it back to the consumer of this library - const storedCTMResult = { x: 0, y: 0 }; - - let isDirty = false; - const transform = { - x: 0, - y: 0, - scale: 1, - }; - - // TODO: likely need to unite pinchSpeed with zoomSpeed - const pinchSpeed = - typeof options.pinchSpeed === 'number' ? options.pinchSpeed : 1; - const bounds = options.bounds; - const maxZoom = - typeof options.maxZoom === 'number' - ? options.maxZoom - : Number.POSITIVE_INFINITY; - const minZoom = typeof options.minZoom === 'number' ? options.minZoom : 0; - - const boundsPadding = - typeof options.boundsPadding === 'number' ? options.boundsPadding : 0.05; - - const speed = - typeof options.zoomSpeed === 'number' - ? options.zoomSpeed - : defaultZoomSpeed; - let transformOrigin = parseTransformOrigin(options.transformOrigin); - - validateBounds(bounds); - - let frameAnimation: number; - let touchInProgress = false; - - // We only need to fire panstart when actual move happens - let panstartFired = false; - - // cache mouse coordinates here - let mouseX: number; - let mouseY: number; - - let pinchZoomLength: number; - - const smoothScroll = kinetic(getPoint, scroll, options.smoothScroll); - - let zoomToAnimation: { cancel: () => void }; - - let multiTouch: boolean; - let paused = false; - - listenForEvents(); - - const api = { - dispose, - moveBy, - moveTo, - smoothMoveTo, - centerOn, - zoomTo: publicZoomTo, - zoomAbs, - - pause, - resume, - isPaused, - - getTransform: getTransformModel, - - setTransform, - - getTransformOrigin, - setTransformOrigin, - }; - - const initialX = - typeof options.initialX === 'number' ? options.initialX : transform.x; - const initialY = - typeof options.initialY === 'number' ? options.initialY : transform.y; - const initialZoom = - typeof options.initialZoom === 'number' - ? options.initialZoom - : transform.scale; - - if ( - initialX != transform.x || - initialY != transform.y || - initialZoom != transform.scale - ) { - zoomAbs(initialX, initialY, initialZoom); - } - - return api; - - function pause() { - releaseEvents(); - paused = true; - } - - function resume() { - if (paused) { - listenForEvents(); - paused = false; - } - } - - function isPaused() { - return paused; - } - - function transformToScreen(x: number, y: number) { - storedCTMResult.x = x; - storedCTMResult.y = y; - - return storedCTMResult; - } - - function setTransform(x: number, y: number, s: number) { - transform.x = x; - transform.y = y; - transform.scale = s; - makeDirty(); - } - - function getTransformModel() { - // TODO: should this be read only? - return transform; - } - - function getTransformOrigin() { - return transformOrigin; - } - - function setTransformOrigin(newTransformOrigin: TransformOrigin) { - transformOrigin = parseTransformOrigin(newTransformOrigin); - } - - function getPoint() { - return { - x: transform.x, - y: transform.y, - }; - } - - function moveTo(x: number, y: number) { - transform.x = x; - transform.y = y; - - keepTransformInsideBounds(); - - makeDirty(); - } - - function moveBy(dx: number, dy: number) { - moveTo(transform.x + dx, transform.y + dy); - } - - function keepTransformInsideBounds() { - const boundingBox = getBoundingBox(); - if (!boundingBox) return; - - let adjusted = false; - const clientRect = getClientRect(); - - let diff = boundingBox.left - clientRect.right; - if (diff > 0) { - transform.x += diff; - adjusted = true; - } - // check the other side: - diff = boundingBox.right - clientRect.left; - if (diff < 0) { - transform.x += diff; - adjusted = true; - } - - // y axis: - diff = boundingBox.top - clientRect.bottom; - if (diff > 0) { - // we adjust transform, so that it matches exactly our bounding box: - // transform.y = boundingBox.top - (boundingBox.height + boundingBox.y) * transform.scale => - // transform.y = boundingBox.top - (clientRect.bottom - transform.y) => - // transform.y = diff + transform.y => - transform.y += diff; - adjusted = true; - } - - diff = boundingBox.bottom - clientRect.top; - if (diff < 0) { - transform.y += diff; - adjusted = true; - } - return adjusted; - } - - /** - * Returns bounding box that should be used to restrict scene movement. - */ - function getBoundingBox() { - if (!bounds) return; // client does not want to restrict movement - - if (typeof bounds === 'boolean') { - // for boolean type we use parent container bounds - const ownerRect = owner.getBoundingClientRect(); - const sceneWidth = ownerRect.width; - const sceneHeight = ownerRect.height; - - return { - left: sceneWidth * boundsPadding, - top: sceneHeight * boundsPadding, - right: sceneWidth * (1 - boundsPadding), - bottom: sceneHeight * (1 - boundsPadding), - }; - } - - return bounds; - } - - function getClientRect() { - const bbox = panController.getBBox(); - const leftTop = client(bbox.left, bbox.top); - - return { - left: leftTop.x, - top: leftTop.y, - right: bbox.width * transform.scale + leftTop.x, - bottom: bbox.height * transform.scale + leftTop.y, - }; - } - - function client(x: number, y: number) { - return { - x: x * transform.scale + transform.x, - y: y * transform.scale + transform.y, - }; - } - - function makeDirty() { - isDirty = true; - - frameAnimation = window.requestAnimationFrame(frame); - } - - function zoomByRatio(clientX: number, clientY: number, ratio: number) { - if (isNaN(clientX) || isNaN(clientY) || isNaN(ratio)) { - throw new Error('zoom requires valid numbers'); - } - - const newScale = transform.scale * ratio; - - if (newScale < minZoom) { - if (transform.scale === minZoom) return; - - ratio = minZoom / transform.scale; - } - if (newScale > maxZoom) { - if (transform.scale === maxZoom) return; - - ratio = maxZoom / transform.scale; - } - - const size = transformToScreen(clientX, clientY); - - transform.x = size.x - ratio * (size.x - transform.x); - transform.y = size.y - ratio * (size.y - transform.y); - - // TODO: https://github.com/anvaka/panzoom/issues/112 - if (bounds && boundsPadding === 1 && minZoom === 1) { - transform.scale *= ratio; - keepTransformInsideBounds(); - } else { - const transformAdjusted = keepTransformInsideBounds(); - if (!transformAdjusted) transform.scale *= ratio; - } - - makeDirty(); - } - - function zoomAbs(clientX: number, clientY: number, zoomLevel: number) { - const ratio = zoomLevel / transform.scale; - zoomByRatio(clientX, clientY, ratio); - } - - function centerOn(ui: SVGElement) { - const parent = ui.ownerSVGElement; - if (!parent) - throw new Error('ui element is required to be within the scene'); - - // TODO: should i use controller's screen CTM? - const clientRect = ui.getBoundingClientRect(); - const cx = clientRect.left + clientRect.width / 2; - const cy = clientRect.top + clientRect.height / 2; - - const container = parent.getBoundingClientRect(); - const dx = container.width / 2 - cx; - const dy = container.height / 2 - cy; - - internalMoveBy(dx, dy); - } - - function smoothMoveTo(x: number, y: number) { - internalMoveBy(x - transform.x, y - transform.y); - } - - function internalMoveBy(dx: number, dy: number) { - return moveBy(dx, dy); - } - - function scroll(x: number, y: number) { - cancelZoomAnimation(); - moveTo(x, y); - } - - function dispose() { - releaseEvents(); - } - - function listenForEvents() { - owner.addEventListener('mousedown', onMouseDown, { passive: true }); - owner.addEventListener('dblclick', onDoubleClick, { passive: false }); - owner.addEventListener('touchstart', onTouch, { passive: true }); - owner.addEventListener('keydown', onKeyDown); - - // Need to listen on the owner container, so that we are not limited - // by the size of the scrollable domElement - owner.addEventListener('wheel', onMouseWheel, { passive: true }); - - makeDirty(); - } - - function releaseEvents() { - owner.removeEventListener('wheel', onMouseWheel); - owner.removeEventListener('mousedown', onMouseDown); - owner.removeEventListener('keydown', onKeyDown); - owner.removeEventListener('dblclick', onDoubleClick); - owner.removeEventListener('touchstart', onTouch); - - if (frameAnimation) { - window.cancelAnimationFrame(frameAnimation); - frameAnimation = 0; - } - - smoothScroll.cancel(); - - releaseDocumentMouse(); - releaseTouches(); - - triggerPanEnd(); - } - - function frame() { - if (isDirty) applyTransform(); - } - - function applyTransform() { - isDirty = false; - - // TODO: Should I allow to cancel this? - panController.applyTransform(transform); - - frameAnimation = 0; - - if (options.onTransform) { - options.onTransform(transform); - } - } - - function onKeyDown(e: KeyboardEvent) { - // let x = 0, - // y = 0, - let z = 0; - if (e.key === 'ArrowUp') { - // y = 1; // up - } else if (e.key === 'ArrowDown') { - // y = -1; // down - } else if (e.key === 'ArrowLeft') { - // x = 1; // left - } else if (e.key === 'ArrowRigh') { - // x = -1; // right - } else if (e.key === '-') { - // DASH or SUBTRACT - z = 1; // `-` - zoom out - } else if (e.key === '=' || e.key === '+') { - // EQUAL SIGN or ADD - z = -1; // `=` - zoom in (equal sign on US layout is under `+`) - } - if (z) { - const scaleMultiplier = getScaleMultiplier(z * 100); - const offset = transformOrigin ? getTransformOriginOffset() : midPoint(); - publicZoomTo(offset.x, offset.y, scaleMultiplier); - } - } - - function midPoint() { - const ownerRect = owner.getBoundingClientRect(); - return { - x: ownerRect.width / 2, - y: ownerRect.height / 2, - }; - } - - function onTouch(e: TouchEvent) { - // let the override the touch behavior - beforeTouch(e); - - if (e.touches.length === 1) { - return handleSingleFingerTouch(e); - } else if (e.touches.length === 2) { - // handleTouchMove() will care about pinch zoom. - pinchZoomLength = getPinchZoomLength(e.touches[0], e.touches[1]); - multiTouch = true; - startTouchListenerIfNeeded(); - } - } - - function beforeTouch(e: TouchEvent) { - e.stopPropagation(); - e.preventDefault(); - } - - function beforeDoubleClick(e: MouseEvent) { - e.preventDefault(); - e.stopPropagation(); - } - - function handleSingleFingerTouch(e: TouchEvent) { - const touch = e.touches[0]; - const offset = getOffsetXY(touch); - const point = transformToScreen(offset.x, offset.y); - mouseX = point.x; - mouseY = point.y; - - smoothScroll.cancel(); - startTouchListenerIfNeeded(); - } - - function startTouchListenerIfNeeded() { - if (touchInProgress) { - // no need to do anything, as we already listen to events; - return; - } - - touchInProgress = true; - document.addEventListener('touchmove', handleTouchMove); - document.addEventListener('touchend', handleTouchEnd); - document.addEventListener('touchcancel', handleTouchEnd); - } - - function handleTouchMove(e: TouchEvent) { - if (e.touches.length === 1) { - e.stopPropagation(); - const touch = e.touches[0]; - - const offset = getOffsetXY(touch); - const point = transformToScreen(offset.x, offset.y); - - const dx = point.x - mouseX; - const dy = point.y - mouseY; - - if (dx !== 0 && dy !== 0) { - triggerPanStart(); - } - mouseX = point.x; - mouseY = point.y; - internalMoveBy(dx, dy); - } else if (e.touches.length === 2) { - // it's a zoom, let's find direction - multiTouch = true; - const t1 = e.touches[0]; - const t2 = e.touches[1]; - const currentPinchLength = getPinchZoomLength(t1, t2); - - // since the zoom speed is always based on distance from 1, we need to apply - // pinch speed only on that distance from 1: - const scaleMultiplier = - 1 + (currentPinchLength / pinchZoomLength - 1) * pinchSpeed; - - const firstTouchPoint = getOffsetXY(t1); - const secondTouchPoint = getOffsetXY(t2); - mouseX = (firstTouchPoint.x + secondTouchPoint.x) / 2; - mouseY = (firstTouchPoint.y + secondTouchPoint.y) / 2; - if (transformOrigin) { - const offset = getTransformOriginOffset(); - mouseX = offset.x; - mouseY = offset.y; - } - - publicZoomTo(mouseX, mouseY, scaleMultiplier); - - pinchZoomLength = currentPinchLength; - e.stopPropagation(); - e.preventDefault(); - } - } - - function handleTouchEnd(e: TouchEvent) { - if (e.touches.length > 0) { - const offset = getOffsetXY(e.touches[0]); - const point = transformToScreen(offset.x, offset.y); - mouseX = point.x; - mouseY = point.y; - } - } - - function getPinchZoomLength(finger1: Touch, finger2: Touch) { - const dx = finger1.clientX - finger2.clientX; - const dy = finger1.clientY - finger2.clientY; - return Math.sqrt(dx * dx + dy * dy); - } - - function onDoubleClick(e: MouseEvent) { - beforeDoubleClick(e); - } - - function onMouseDown(e: MouseEvent) { - if (touchInProgress) { - // modern browsers will fire mousedown for touch events too - // we do not want this: touch is handled separately. - e.stopPropagation(); - return false; - } - - if (e.target !== owner && e.target !== domElement) return; - - // for IE, left click == 1 - // for Firefox, left click == 0 - const isLeftButton = - (e.button === 1 && window.event !== null) || e.button === 0; - if (!isLeftButton) return; - - smoothScroll.cancel(); - - const offset = getOffsetXY(e); - const point = transformToScreen(offset.x, offset.y); - mouseX = point.x; - mouseY = point.y; - - // We need to listen on document itself, since mouse can go outside of the - // window, and we will loose it - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); - - return false; - } - - function onMouseMove(e: MouseEvent) { - // no need to worry about mouse events when touch is happening - if (touchInProgress) return; - - if (e.ctrlKey) return; - - triggerPanStart(); - - const offset = getOffsetXY(e); - const point = transformToScreen(offset.x, offset.y); - const dx = point.x - mouseX; - const dy = point.y - mouseY; - - mouseX = point.x; - mouseY = point.y; - - internalMoveBy(dx, dy); - } - - function onMouseUp() { - triggerPanEnd(); - releaseDocumentMouse(); - } - - function releaseDocumentMouse() { - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('mouseup', onMouseUp); - panstartFired = false; - } - - function releaseTouches() { - document.removeEventListener('touchmove', handleTouchMove); - document.removeEventListener('touchend', handleTouchEnd); - document.removeEventListener('touchcancel', handleTouchEnd); - panstartFired = false; - multiTouch = false; - touchInProgress = false; - } - - function onMouseWheel(e: WheelEvent) { - smoothScroll.cancel(); - - let delta = e.deltaY; - if (e.deltaMode > 0) delta *= 100; - - const scaleMultiplier = getScaleMultiplier(delta); - - if (scaleMultiplier !== 1) { - const offset = transformOrigin - ? getTransformOriginOffset() - : getOffsetXY(e); - publicZoomTo(offset.x, offset.y, scaleMultiplier); - } - } - - function getOffsetXY(e: MouseEvent | Touch) { - // let offsetX, offsetY; - // I tried using e.offsetX, but that gives wrong results for svg, when user clicks on a path. - const ownerRect = owner.getBoundingClientRect(); - const offsetX = e.clientX - ownerRect.left; - const offsetY = e.clientY - ownerRect.top; - - return { x: offsetX, y: offsetY }; - } - - function getTransformOriginOffset() { - const ownerRect = owner.getBoundingClientRect(); - return { - x: ownerRect.width * transformOrigin.x, - y: ownerRect.height * transformOrigin.y, - }; - } - - function publicZoomTo( - clientX: number, - clientY: number, - scaleMultiplier: number, - ) { - smoothScroll.cancel(); - cancelZoomAnimation(); - return zoomByRatio(clientX, clientY, scaleMultiplier); - } - - function cancelZoomAnimation() { - if (zoomToAnimation) { - zoomToAnimation.cancel(); - zoomToAnimation = null; - } - } - - function getScaleMultiplier(delta: number) { - const sign = Math.sign(delta); - const deltaAdjustedSpeed = Math.min(0.25, Math.abs((speed * delta) / 128)); - return 1 - sign * deltaAdjustedSpeed; - } - - function triggerPanStart() { - if (!panstartFired) { - panstartFired = true; - smoothScroll.start(); - } - } - - function triggerPanEnd() { - if (panstartFired) { - // we should never run smooth scrolling if it was multiTouch (pinch zoom animation): - if (!multiTouch) smoothScroll.stop(); - } - } -} - -function parseTransformOrigin(options: TransformOrigin) { - if (!options) return; - if (typeof options === 'object') { - if (!isNumber(options.x) || !isNumber(options.y)) failTransformOrigin(); - return options; - } - - failTransformOrigin(); -} - -function failTransformOrigin() { - throw new Error( - [ - 'Cannot parse transform origin.', - 'Some good examples:', - ' "center center" can be achieved with {x: 0.5, y: 0.5}', - ' "top center" can be achieved with {x: 0.5, y: 0}', - ' "bottom right" can be achieved with {x: 1, y: 1}', - ].join('\n'), - ); -} - -function validateBounds(bounds: boolean | Bounds) { - if (!bounds) return; - if (typeof bounds === 'boolean') return; // this is okay - // otherwise need to be more thorough: - const validBounds = - isNumber(bounds.left) && - isNumber(bounds.top) && - isNumber(bounds.bottom) && - isNumber(bounds.right); - - if (!validBounds) - throw new Error( - 'Bounds object is not valid. It can be: ' + - 'undefined, boolean (true|false) or an object {left, top, right, bottom}', - ); -} - -function isNumber(x: number) { - return Number.isFinite(x); -} - -// IE 11 does not support isNaN: -function isNaN(value: unknown) { - if (Number.isNaN) { - return Number.isNaN(value); - } - - return value !== value; -} diff --git a/app/src/lib/panzoom/kinetic.ts b/app/src/lib/panzoom/kinetic.ts deleted file mode 100644 index f0a4101..0000000 --- a/app/src/lib/panzoom/kinetic.ts +++ /dev/null @@ -1,146 +0,0 @@ -/** - * Allows smooth kinetic scrolling of the surface - */ -export default function kinetic( - getPoint: () => { x: number; y: number }, - scroll: (x: number, y: number) => void, - settings: Record, -) { - if (typeof settings !== 'object') { - // setting could come as boolean, we should ignore it, and use an object. - settings = {}; - } - - const minVelocity = - typeof settings.minVelocity === 'number' ? settings.minVelocity : 5; - const amplitude = - typeof settings.amplitude === 'number' ? settings.amplitude : 0.25; - const cancelAnimationFrame = - typeof settings.cancelAnimationFrame === 'function' - ? settings.cancelAnimationFrame - : getCancelAnimationFrame(); - const requestAnimationFrame = - typeof settings.requestAnimationFrame === 'function' - ? settings.requestAnimationFrame - : getRequestAnimationFrame(); - - let lastPoint: { x: number; y: number }; - let timestamp: number; - const timeConstant = 342; - - let ticker: unknown; - let vx: number, targetX: number, ax: number; - let vy: number, targetY: number, ay: number; - - let raf: unknown; - - return { - start: start, - stop: stop, - cancel: dispose, - }; - - function dispose() { - cancelAnimationFrame(ticker); - cancelAnimationFrame(raf); - } - - function start() { - lastPoint = getPoint(); - - ax = ay = vx = vy = 0; - timestamp = Date.now(); - - cancelAnimationFrame(ticker); - cancelAnimationFrame(raf); - - // we start polling the point position to accumulate velocity - // Once we stop(), we will use accumulated velocity to keep scrolling - // an object. - ticker = requestAnimationFrame(track); - } - - function track() { - const now = Date.now(); - const elapsed = now - timestamp; - timestamp = now; - - const currentPoint = getPoint(); - - const dx = currentPoint.x - lastPoint.x; - const dy = currentPoint.y - lastPoint.y; - - lastPoint = currentPoint; - - const dt = 1000 / (1 + elapsed); - - // moving average - vx = 0.8 * dx * dt + 0.2 * vx; - vy = 0.8 * dy * dt + 0.2 * vy; - - ticker = requestAnimationFrame(track); - } - - function stop() { - cancelAnimationFrame(ticker); - cancelAnimationFrame(raf); - - const currentPoint = getPoint(); - - targetX = currentPoint.x; - targetY = currentPoint.y; - timestamp = Date.now(); - - if (vx < -minVelocity || vx > minVelocity) { - ax = amplitude * vx; - targetX += ax; - } - - if (vy < -minVelocity || vy > minVelocity) { - ay = amplitude * vy; - targetY += ay; - } - - raf = requestAnimationFrame(autoScroll); - } - - function autoScroll() { - const elapsed = Date.now() - timestamp; - - let moving = false; - let dx = 0; - let dy = 0; - - if (ax) { - dx = -ax * Math.exp(-elapsed / timeConstant); - - if (dx > 0.5 || dx < -0.5) moving = true; - else dx = ax = 0; - } - - if (ay) { - dy = -ay * Math.exp(-elapsed / timeConstant); - - if (dy > 0.5 || dy < -0.5) moving = true; - else dy = ay = 0; - } - - if (moving) { - scroll(targetX + dx, targetY + dy); - raf = requestAnimationFrame(autoScroll); - } - } -} - -function getCancelAnimationFrame() { - if (typeof cancelAnimationFrame === 'function') return cancelAnimationFrame; - return clearTimeout; -} - -function getRequestAnimationFrame() { - if (typeof requestAnimationFrame === 'function') return requestAnimationFrame; - - return function (handler: () => void) { - return setTimeout(handler, 16); - }; -} diff --git a/app/src/routes/+page.svelte b/app/src/routes/+page.svelte index 8d048de..058f493 100644 --- a/app/src/routes/+page.svelte +++ b/app/src/routes/+page.svelte @@ -1,15 +1,14 @@ -
+
-
- - - - +
+
header
+ + + + + +
diff --git a/app/src/routes/app.css b/app/src/routes/app.css index e46dfb4..8db7309 100644 --- a/app/src/routes/app.css +++ b/app/src/routes/app.css @@ -59,6 +59,10 @@ body.theme-catppuccin { --background-color-darker: #11111b; } +body { + margin: 0; +} + /* canvas { */ /* display: none !important; */ /* } */ diff --git a/app/svelte.config.js b/app/svelte.config.js index 78a97fe..2a2b3bc 100644 --- a/app/svelte.config.js +++ b/app/svelte.config.js @@ -3,16 +3,16 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; /** @type {import('@sveltejs/kit').Config} */ const config = { - // Consult https://kit.svelte.dev/docs/integrations#preprocessors - // for more information about preprocessors - preprocess: vitePreprocess(), + // Consult https://kit.svelte.dev/docs/integrations#preprocessors + // for more information about preprocessors + preprocess: vitePreprocess(), - kit: { - // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. - // If your environment is not supported or you settled on a specific environment, switch out the adapter. - // See https://kit.svelte.dev/docs/adapters for more information about adapters. - adapter: adapter() - } + kit: { + // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. + // If your environment is not supported or you settled on a specific environment, switch out the adapter. + // See https://kit.svelte.dev/docs/adapters for more information about adapters. + adapter: adapter() + } }; export default config; diff --git a/app/vite.config.ts b/app/vite.config.ts index 90d2ca5..75dd485 100644 --- a/app/vite.config.ts +++ b/app/vite.config.ts @@ -5,6 +5,11 @@ import wasm from "vite-plugin-wasm"; export default defineConfig({ plugins: [sveltekit(), glsl(), wasm()], + + server: { + port: 8080, + }, + ssr: { noExternal: ['three'], } diff --git a/package.json b/package.json index fd275e0..92bd206 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "scripts": { - "build:nodes": "pnpm -r --filter './nodes/**' build" + "build:nodes": "pnpm -r --filter './nodes/**' build", + "dev": "pnpm -r --filter 'app' --filter './packages/node-registry' dev" } } diff --git a/packages/node-registry/svelte.config.js b/packages/node-registry/svelte.config.js index 2b35fe1..4b6423e 100644 --- a/packages/node-registry/svelte.config.js +++ b/packages/node-registry/svelte.config.js @@ -3,16 +3,18 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; /** @type {import('@sveltejs/kit').Config} */ const config = { - // Consult https://kit.svelte.dev/docs/integrations#preprocessors - // for more information about preprocessors - preprocess: vitePreprocess(), + // Consult https://kit.svelte.dev/docs/integrations#preprocessors + // for more information about preprocessors + preprocess: vitePreprocess(), - kit: { - // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. - // If your environment is not supported or you settled on a specific environment, switch out the adapter. - // See https://kit.svelte.dev/docs/adapters for more information about adapters. - adapter: adapter() - } + + + kit: { + // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. + // If your environment is not supported or you settled on a specific environment, switch out the adapter. + // See https://kit.svelte.dev/docs/adapters for more information about adapters. + adapter: adapter() + } }; export default config; diff --git a/packages/node-registry/vite.config.ts b/packages/node-registry/vite.config.ts index a2bcd5c..9f4259e 100644 --- a/packages/node-registry/vite.config.ts +++ b/packages/node-registry/vite.config.ts @@ -4,6 +4,10 @@ import wasm from 'vite-plugin-wasm'; export default defineConfig({ plugins: [sveltekit(), wasm()], + + server: { + port: 3001, + }, test: { include: ['src/**/*.{test,spec}.{js,ts}'] }