headless-vpl 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,35 @@
1
+ import Connector from './core/Connector';
2
+ import Workspace from './core/Workspace';
3
+ import Position from './core/Position';
4
+ import Edge from './core/Edge';
5
+ import Container from './core/Container';
6
+ import AutoLayout from './core/AutoLayout';
7
+ import { EventBus } from './core/EventBus';
8
+ import { SelectionManager } from './core/SelectionManager';
9
+ import { History } from './core/History';
10
+ import { MoveCommand, AddCommand, RemoveCommand, ConnectCommand } from './core/commands';
11
+ import { SvgRenderer } from './rendering/SvgRenderer';
12
+ import { getDistance, getAngle } from './util/distance';
13
+ import { screenToWorld, worldToScreen } from './util/viewport';
14
+ import { getStraightPath, getBezierPath, getStepPath, getSmoothStepPath } from './util/edgePath';
15
+ import { createMarqueeRect, getElementsInMarquee, getElementsInScreenMarquee } from './util/marquee';
16
+ import { snapToGrid, snapDeltaToGrid } from './util/snapToGrid';
17
+ import { copyElements, calculatePastePositions, pasteElements } from './util/clipboard';
18
+ import { KeyboardManager } from './util/keyboard';
19
+ import { computeAutoPan } from './util/autoPan';
20
+ import { detectResizeHandle, beginResize, applyResize } from './util/resize';
21
+ import { EdgeBuilder, isConnectorHit, findNearestConnector } from './util/edgeBuilder';
22
+ export { Connector, Workspace, Position, Edge, Container, AutoLayout, EventBus, SelectionManager, History, MoveCommand, AddCommand, RemoveCommand, ConnectCommand, SvgRenderer, getDistance, getAngle, screenToWorld, worldToScreen, getStraightPath, getBezierPath, getStepPath, getSmoothStepPath, createMarqueeRect, getElementsInMarquee, getElementsInScreenMarquee, snapToGrid, snapDeltaToGrid, copyElements, calculatePastePositions, pasteElements, KeyboardManager, computeAutoPan, detectResizeHandle, beginResize, applyResize, EdgeBuilder, isConnectorHit, findNearestConnector, };
23
+ export type { IWorkspaceElement, IEdge, VplEvent, VplEventType, Viewport, SizingMode, Padding } from './core/types';
24
+ export type { EdgeType, MarkerType, EdgeMarker, ResizeHandleDirection } from './core/types';
25
+ export type { IPosition } from './core/Position';
26
+ export type { Command } from './core/History';
27
+ export type { EdgePathResult } from './util/edgePath';
28
+ export type { MarqueeRect, MarqueeMode, MarqueeElement } from './util/marquee';
29
+ export type { ClipboardData } from './util/clipboard';
30
+ export type { KeyBinding } from './util/keyboard';
31
+ export type { CanvasBounds, AutoPanResult } from './util/autoPan';
32
+ export type { ResizableElement, ResizeState } from './util/resize';
33
+ export type { EdgeBuilderConfig } from './util/edgeBuilder';
34
+ export { SnapConnection, childOnly, parentOnly, either } from './util/snap';
35
+ export type { ConnectionValidator, SnapStrategy, SnapConnectionConfig } from './util/snap';
@@ -0,0 +1,35 @@
1
+ import type Workspace from '../core/Workspace';
2
+ /**
3
+ * EventBus を購読し、Workspace の要素を SVG として描画する。
4
+ * core/ から分離された描画レイヤー。
5
+ */
6
+ export declare class SvgRenderer {
7
+ private svgRoot;
8
+ private viewportGroup;
9
+ private defsElement;
10
+ private workspace;
11
+ private elementMap;
12
+ private markerDefs;
13
+ constructor(svgRoot: SVGSVGElement, workspace: Workspace);
14
+ private updateViewportTransform;
15
+ private onAdd;
16
+ private onMove;
17
+ private onUpdate;
18
+ private onRemove;
19
+ private onSelect;
20
+ private onDeselect;
21
+ private ensureElement;
22
+ private createElement;
23
+ private updateElement;
24
+ private getId;
25
+ private createContainerRect;
26
+ private updateContainerRect;
27
+ private createConnectorCircle;
28
+ private updateConnectorCircle;
29
+ private getMarkerId;
30
+ private ensureMarkerDef;
31
+ private createEdgeGroup;
32
+ private updateEdgePath;
33
+ private createAutoLayoutRect;
34
+ private updateAutoLayoutRect;
35
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * 毎フレーム実行されるアニメーションループのユーティリティ関数
3
+ *
4
+ * @param onFrame 各フレームで実行するコールバック関数
5
+ * @param deltaTime 前回フレームからの経過時間 (ms)
6
+ * @param frame 現在のフレーム数
7
+ */
8
+ export declare function animate(callback: (deltaTime: number, frame: number) => void): void;
@@ -0,0 +1,23 @@
1
+ import type { IPosition } from '../core/Position';
2
+ export type CanvasBounds = {
3
+ x: number;
4
+ y: number;
5
+ width: number;
6
+ height: number;
7
+ };
8
+ export type AutoPanResult = {
9
+ dx: number;
10
+ dy: number;
11
+ active: boolean;
12
+ };
13
+ /**
14
+ * マウスがキャンバス端に近いとき、パン方向と速度を計算する。
15
+ * 端からの距離に応じた線形速度を返す。
16
+ *
17
+ * @param mousePos スクリーン座標でのマウス位置
18
+ * @param bounds キャンバスの矩形
19
+ * @param isDragging ドラッグ中かどうか
20
+ * @param threshold 端からの判定距離(px)
21
+ * @param speed パン速度の最大値(px/frame)
22
+ */
23
+ export declare function computeAutoPan(mousePos: IPosition, bounds: CanvasBounds, isDragging: boolean, threshold?: number, speed?: number): AutoPanResult;
@@ -0,0 +1,20 @@
1
+ import type { IPosition } from '../core/Position';
2
+ export type ClipboardData = {
3
+ elements: Record<string, unknown>[];
4
+ };
5
+ /**
6
+ * 要素を JSON としてシリアライズし、ClipboardData を生成する。
7
+ */
8
+ export declare function copyElements(elements: {
9
+ toJSON(): Record<string, unknown>;
10
+ }[]): ClipboardData;
11
+ /**
12
+ * 貼り付け時の新しい位置を計算する。
13
+ * 元の位置から offset 分ずらす。
14
+ */
15
+ export declare function calculatePastePositions(data: ClipboardData, offset?: IPosition): IPosition[];
16
+ /**
17
+ * ClipboardData から要素を再生成する。
18
+ * factory パターンにより、具体的な型の生成はユーザー責務。
19
+ */
20
+ export declare function pasteElements<T>(data: ClipboardData, factory: (json: Record<string, unknown>, position: IPosition) => T, offset?: IPosition): T[];
@@ -0,0 +1,3 @@
1
+ import Container from '../core/Container';
2
+ import { IPosition } from '../core/Position';
3
+ export declare function isCollision(container: Container, position: IPosition): boolean;
@@ -0,0 +1,3 @@
1
+ import { IPosition } from '../core/Position';
2
+ export declare function getDistance(point1: IPosition, point2: IPosition): number;
3
+ export declare function getAngle(point1: IPosition, point2: IPosition): number;
@@ -0,0 +1,18 @@
1
+ import Container from '../core/Container';
2
+ import { IPosition } from '../core/Position';
3
+ import { getMouseState } from './mouse';
4
+ /**
5
+ * 複数のコンテナーに対してドラッグ&ドロップの更新を行います。
6
+ *
7
+ * @param containers - ドラッグ対象のコンテナーの配列
8
+ * @param dx - 前フレームからのX軸方向の移動量
9
+ * @param dy - 前フレームからのY軸方向の移動量
10
+ * @param mousePosition - マウスの現在位置
11
+ * @param mouseState - マウスの状態(leftButton は 'down' / 'up')
12
+ * @param dragEligible - マウスクリック開始時にコンテナー上でクリックされたかを示すフラグ
13
+ * @param currentDragContainers - 現在ドラッグ中のコンテナーの配列
14
+ * @param allowMultiple - 複数のコンテナーをドラッグ可能にするかどうかのフラグ
15
+ *
16
+ * @returns 更新後のドラッグ中コンテナーの配列。ドラッグしていなければ空の配列を返します。
17
+ */
18
+ export declare function DragAndDrop(containers: Container[], delta: IPosition, mouseState: getMouseState, dragEligible: boolean, currentDragContainers: Container[], allowMultiple?: boolean, callback?: () => void): Container[];
@@ -0,0 +1,13 @@
1
+ export declare class DomController {
2
+ private element;
3
+ private x;
4
+ private y;
5
+ constructor(selector: string);
6
+ move(x: number, y: number): void;
7
+ moveBy(dx: number, dy: number): void;
8
+ getPosition(): {
9
+ x: number;
10
+ y: number;
11
+ };
12
+ private updatePosition;
13
+ }
@@ -0,0 +1,65 @@
1
+ import type Connector from '../core/Connector';
2
+ import type { IPosition } from '../core/Position';
3
+ import type Workspace from '../core/Workspace';
4
+ import type { EdgeType } from '../core/types';
5
+ import Edge from '../core/Edge';
6
+ /**
7
+ * コネクターがマウス位置のヒット範囲内かを判定する。
8
+ */
9
+ export declare function isConnectorHit(mousePos: IPosition, connector: {
10
+ position: IPosition;
11
+ }, hitRadius?: number): boolean;
12
+ /**
13
+ * マウス位置に最も近いコネクターを返す。hitRadius 内のもののみ。
14
+ */
15
+ export declare function findNearestConnector(mousePos: IPosition, connectors: Array<{
16
+ position: IPosition;
17
+ }>, hitRadius?: number): {
18
+ connector: (typeof connectors)[number];
19
+ distance: number;
20
+ } | null;
21
+ export type EdgeBuilderConfig = {
22
+ workspace: Workspace;
23
+ hitRadius?: number;
24
+ edgeType?: EdgeType;
25
+ onPreview?: (path: string, from: IPosition, to: IPosition) => void;
26
+ onComplete?: (edge: Edge) => void;
27
+ onCancel?: () => void;
28
+ };
29
+ /**
30
+ * コネクターからドラッグして Edge を作成するビルダー。
31
+ * Headless 設計に従い DOM 操作なし。
32
+ */
33
+ export declare class EdgeBuilder {
34
+ private workspace;
35
+ private hitRadius;
36
+ private edgeType;
37
+ private onPreview?;
38
+ private onComplete?;
39
+ private onCancel?;
40
+ private _active;
41
+ private _startConnector;
42
+ private _previewPath;
43
+ constructor(config: EdgeBuilderConfig);
44
+ get active(): boolean;
45
+ get previewPath(): string | null;
46
+ get startConnector(): Connector | null;
47
+ /**
48
+ * ドラッグ開始。出力コネクターからスタート。
49
+ */
50
+ start(connector: Connector): void;
51
+ /**
52
+ * マウス移動ごとにプレビューパスを更新。
53
+ */
54
+ update(mousePos: IPosition): void;
55
+ /**
56
+ * 入力コネクターにドロップして Edge を作成。
57
+ */
58
+ complete(connector: Connector): void;
59
+ /**
60
+ * キャンセル。
61
+ */
62
+ cancel(): void;
63
+ private reset;
64
+ private computePath;
65
+ }
@@ -0,0 +1,24 @@
1
+ import type { IPosition } from '../core/Position';
2
+ export type EdgePathResult = {
3
+ path: string;
4
+ labelPosition: IPosition;
5
+ };
6
+ /**
7
+ * 直線パス。
8
+ */
9
+ export declare function getStraightPath(start: IPosition, end: IPosition): EdgePathResult;
10
+ /**
11
+ * ベジェ曲線パス(3次)。
12
+ * 制御点は始点・終点の中間 X に配置。
13
+ */
14
+ export declare function getBezierPath(start: IPosition, end: IPosition): EdgePathResult;
15
+ /**
16
+ * ステップパス(直角折れ線)。
17
+ * 中間 X で折れる。
18
+ */
19
+ export declare function getStepPath(start: IPosition, end: IPosition): EdgePathResult;
20
+ /**
21
+ * スムースステップパス(角丸の折れ線)。
22
+ * borderRadius で角丸のサイズを制御。
23
+ */
24
+ export declare function getSmoothStepPath(start: IPosition, end: IPosition, borderRadius?: number): EdgePathResult;
@@ -0,0 +1,21 @@
1
+ export type KeyBinding = {
2
+ key: string;
3
+ modifiers?: ('ctrl' | 'shift' | 'alt')[];
4
+ handler: (event: KeyboardEvent) => void;
5
+ };
6
+ /**
7
+ * キーボードショートカットを管理するクラス。
8
+ * Ctrl/Meta を統一し、Mac/Windows の互換性を保証する。
9
+ */
10
+ export declare class KeyboardManager {
11
+ private bindings;
12
+ private element;
13
+ private listener;
14
+ constructor(element: HTMLElement);
15
+ bind(binding: KeyBinding): void;
16
+ unbind(key: string, modifiers?: ('ctrl' | 'shift' | 'alt')[]): void;
17
+ destroy(): void;
18
+ private handleKeyDown;
19
+ private matches;
20
+ private modifiersMatch;
21
+ }
@@ -0,0 +1,30 @@
1
+ import type { IPosition } from '../core/Position';
2
+ import type { Viewport } from '../core/types';
3
+ export type MarqueeRect = {
4
+ x: number;
5
+ y: number;
6
+ width: number;
7
+ height: number;
8
+ };
9
+ export type MarqueeMode = 'full' | 'partial';
10
+ export type MarqueeElement = {
11
+ id: string;
12
+ position: IPosition;
13
+ width: number;
14
+ height: number;
15
+ };
16
+ /**
17
+ * 2 点からマーキー矩形を生成する。
18
+ * どちらの方向にドラッグしても正しい矩形が返る。
19
+ */
20
+ export declare function createMarqueeRect(start: IPosition, end: IPosition): MarqueeRect;
21
+ /**
22
+ * マーキー矩形内にある要素をフィルタリングする。
23
+ * mode: 'full' — 完全に内側にある要素のみ
24
+ * mode: 'partial' — 一部でも重なる要素を含む
25
+ */
26
+ export declare function getElementsInMarquee<T extends MarqueeElement>(elements: readonly T[], marquee: MarqueeRect, mode?: MarqueeMode): T[];
27
+ /**
28
+ * Screen 座標で指定されたマーキーを World 座標に変換してフィルタリングする。
29
+ */
30
+ export declare function getElementsInScreenMarquee<T extends MarqueeElement>(elements: readonly T[], screenStart: IPosition, screenEnd: IPosition, viewport: Viewport, mode?: MarqueeMode): T[];
@@ -0,0 +1,16 @@
1
+ import { IPosition } from '../core/Position';
2
+ export declare function getMousePosition(parent: HTMLElement): IPosition;
3
+ export declare function getPositionDelta(currentPosition: IPosition, previousPosition: IPosition): IPosition;
4
+ export type MouseState = {
5
+ leftButton: 'down' | 'up';
6
+ };
7
+ type getMouseStateProps = {
8
+ mousedown?: (mouseState: MouseState, mousePosition: IPosition, event: MouseEvent) => void;
9
+ mouseup?: (mouseState: MouseState, mousePosition: IPosition, event: MouseEvent) => void;
10
+ };
11
+ export type getMouseState = {
12
+ buttonState: MouseState;
13
+ mousePosition: IPosition;
14
+ };
15
+ export declare function getMouseState(element: HTMLElement, handlers: getMouseStateProps): getMouseState;
16
+ export {};
@@ -0,0 +1,5 @@
1
+ import Container from '../core/Container';
2
+ export declare function moveGroup(containers: Container[], delta: {
3
+ x: number;
4
+ y: number;
5
+ }): void;
@@ -0,0 +1,53 @@
1
+ import type { IPosition } from '../core/Position';
2
+ import type { ResizeHandleDirection } from '../core/types';
3
+ export type ResizableElement = {
4
+ position: IPosition;
5
+ width: number;
6
+ height: number;
7
+ minWidth?: number;
8
+ maxWidth?: number;
9
+ minHeight?: number;
10
+ maxHeight?: number;
11
+ };
12
+ export type ResizeState = {
13
+ handle: ResizeHandleDirection;
14
+ startMousePos: IPosition;
15
+ startBounds: {
16
+ x: number;
17
+ y: number;
18
+ width: number;
19
+ height: number;
20
+ };
21
+ };
22
+ /**
23
+ * マウス位置が要素のどのリサイズハンドルにヒットしているか判定する。
24
+ * handleSize は各辺からの判定距離。
25
+ */
26
+ export declare function detectResizeHandle(mousePos: IPosition, element: {
27
+ position: IPosition;
28
+ width: number;
29
+ height: number;
30
+ }, handleSize?: number): ResizeHandleDirection | null;
31
+ /**
32
+ * リサイズ開始時の状態を記録する。
33
+ */
34
+ export declare function beginResize(handle: ResizeHandleDirection, mousePos: IPosition, element: {
35
+ position: IPosition;
36
+ width: number;
37
+ height: number;
38
+ }): ResizeState;
39
+ /**
40
+ * リサイズを適用し、新しい位置とサイズを返す。
41
+ * min/max 制約を尊重する。
42
+ */
43
+ export declare function applyResize(mousePos: IPosition, state: ResizeState, constraints?: {
44
+ minWidth?: number;
45
+ maxWidth?: number;
46
+ minHeight?: number;
47
+ maxHeight?: number;
48
+ }): {
49
+ x: number;
50
+ y: number;
51
+ width: number;
52
+ height: number;
53
+ };
@@ -0,0 +1,68 @@
1
+ import { Container, Position } from '../../headless-vpl';
2
+ import { getMouseState } from './mouse';
3
+ import Workspace from '../core/Workspace';
4
+ /**
5
+ * 接続バリデーター。snap の前に呼ばれ、false を返すと接続を拒否する。
6
+ */
7
+ export type ConnectionValidator = () => boolean;
8
+ /**
9
+ * 指定されたソースとターゲットのコネクタ間で、ソースコンテナを動かしてスナップを試みます。
10
+ *
11
+ * @param source ソースコンテナ(スナップ対象)
12
+ * @param sourcePosition ソースの座標
13
+ * @param targetPosition ターゲットの座標
14
+ * @param mouseState 現在のマウス状態。左ボタンが 'up' の場合にのみスナップを試みる。
15
+ * @param snapDistance スナップを発動する距離の閾値
16
+ * @param onSnap スナップが成功した場合に実行するコールバック
17
+ * @param onFail スナップが失敗した場合に実行するコールバック
18
+ * @param validator 接続の可否を判定するバリデーター
19
+ * @returns スナップが実行された場合は true を返します。
20
+ */
21
+ export declare function snap(source: Container, sourcePosition: Position, targetPosition: Position, mouseState: getMouseState, snapDistance?: number, onSnap?: () => void, onFail?: () => void, validator?: ConnectionValidator): boolean;
22
+ /**
23
+ * スナップを試みるかどうかの判定関数。
24
+ * source(子)と target(親)のどちらがドラッグされているかで制御できる。
25
+ */
26
+ export type SnapStrategy = (source: Container, target: Container, dragContainers: Container[]) => boolean;
27
+ /** デフォルト: 子(source)が親に近づいた時だけスナップ */
28
+ export declare const childOnly: SnapStrategy;
29
+ /** 親(target)が子に近づいた時だけスナップ */
30
+ export declare const parentOnly: SnapStrategy;
31
+ /** どちらが近づいてもスナップ */
32
+ export declare const either: SnapStrategy;
33
+ export type SnapConnectionConfig = {
34
+ source: Container;
35
+ sourcePosition: Position;
36
+ target: Container;
37
+ targetPosition: Position;
38
+ workspace: Workspace;
39
+ snapDistance?: number;
40
+ strategy?: SnapStrategy;
41
+ validator?: ConnectionValidator;
42
+ };
43
+ /**
44
+ * スナップ接続の状態管理を統合するクラス。
45
+ * snap() + SnapStrategy + Parent/Children 管理 + イベント発火をまとめる。
46
+ */
47
+ export declare class SnapConnection {
48
+ readonly source: Container;
49
+ readonly sourcePosition: Position;
50
+ readonly target: Container;
51
+ readonly targetPosition: Position;
52
+ readonly workspace: Workspace;
53
+ readonly snapDistance: number;
54
+ readonly strategy: SnapStrategy;
55
+ readonly validator?: ConnectionValidator;
56
+ private _locked;
57
+ private _hasFailed;
58
+ private _destroyed;
59
+ private _lastDragContainers;
60
+ constructor(config: SnapConnectionConfig);
61
+ get locked(): boolean;
62
+ get destroyed(): boolean;
63
+ destroy(): void;
64
+ tick(mouseState: getMouseState, dragContainers: Container[]): void;
65
+ unlock(): void;
66
+ private onSnap;
67
+ private onFail;
68
+ }
@@ -0,0 +1,10 @@
1
+ import type { IPosition } from '../core/Position';
2
+ /**
3
+ * 座標をグリッドの倍数に丸める。
4
+ */
5
+ export declare function snapToGrid(position: IPosition, gridSize: number): IPosition;
6
+ /**
7
+ * 移動量をグリッドの倍数に丸める。
8
+ * DnD の delta 前処理として使う。
9
+ */
10
+ export declare function snapDeltaToGrid(delta: IPosition, gridSize: number): IPosition;
@@ -0,0 +1,4 @@
1
+ import type { IPosition } from '../core/Position';
2
+ import type { Viewport } from '../core/types';
3
+ export declare function screenToWorld(screen: IPosition, viewport: Viewport): IPosition;
4
+ export declare function worldToScreen(world: IPosition, viewport: Viewport): IPosition;
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "headless-vpl",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "A headless library for building visual programming languages",
6
+ "module": "./dist/headless-vpl.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/headless-vpl.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "LICENSE",
17
+ "README.md"
18
+ ],
19
+ "license": "MIT",
20
+ "keywords": [
21
+ "visual-programming",
22
+ "vpl",
23
+ "headless",
24
+ "drag-and-drop",
25
+ "node-editor",
26
+ "block-programming"
27
+ ],
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/headless-vpl/headless-vpl"
31
+ },
32
+ "scripts": {
33
+ "dev": "vite",
34
+ "build": "tsc -b && vite build",
35
+ "build:lib": "vite build --config vite.lib.config.ts && tsc -p tsconfig.lib.json",
36
+ "prepublishOnly": "npm run build:lib",
37
+ "lint": "eslint .",
38
+ "preview": "vite preview"
39
+ },
40
+ "devDependencies": {
41
+ "@eslint/js": "^9.19.0",
42
+ "@types/react": "^19.0.8",
43
+ "@types/react-dom": "^19.0.3",
44
+ "@vitejs/plugin-react-swc": "^3.5.0",
45
+ "eslint": "^9.19.0",
46
+ "eslint-plugin-react-hooks": "^5.0.0",
47
+ "eslint-plugin-react-refresh": "^0.4.18",
48
+ "globals": "^15.14.0",
49
+ "react": "^19.0.0",
50
+ "react-dom": "^19.0.0",
51
+ "typescript": "~5.7.2",
52
+ "typescript-eslint": "^8.22.0",
53
+ "vite": "^6.1.0"
54
+ }
55
+ }