@michael_home/workflow-engine-vue 1.0.1 → 1.0.2

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.
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ (function(g,t){typeof exports=="object"&&typeof module<"u"?t(exports,require("vue"),require("@michael_home/workflow-engine-core"),require("@michael_home/workflow-engine-svg-renderer")):typeof define=="function"&&define.amd?define(["exports","vue","@michael_home/workflow-engine-core","@michael_home/workflow-engine-svg-renderer"],t):(g=typeof globalThis<"u"?globalThis:g||self,t(g.WorkflowVue={},g.Vue,g.WorkflowEngineCore,g.WorkflowEngineSvgRenderer))})(this,function(g,t,h,M){"use strict";const A={class:"zoom-controls"},H={class:"zoom-text"},j=["viewBox"],q=["transform"],G={class:"edge-layer"},U=["d","onMousedown"],K=["d","stroke","stroke-dasharray","onMousedown"],Z=["points","fill","stroke","onMousedown"],J=["x","y","fill"],Q=["d"],ee=["points"],te={class:"node-layer"},oe=["transform"],ne=["x","y","width","height","fill","onMousedown"],se=["x","y","width","height","rx","ry","fill","onMousedown"],re=["points","fill","onMousedown"],ie=["x","y","width","height"],ce={key:0,width:"14",height:"14",class:"node-icon-dom","aria-hidden":"true"},le=["href","fill"],de={key:1,class:"node-icon-fallback"},ae={class:"node-title-text"},he=["cx","cy","onMousedown"],fe=((a,I)=>{const r=a.__vccOpts||a;for(const[f,d]of I)r[f]=d;return r})(t.defineComponent({__name:"CanvasView",props:{nodes:{},edges:{},selectedNodeId:{},selectedEdgeId:{},width:{},height:{},panX:{default:0},panY:{default:0},zoom:{default:1},snapToGrid:{type:Boolean,default:!0},gridSize:{default:10},alignThreshold:{default:10},portHitRadius:{default:14},portDragThreshold:{default:4}},emits:["selectionChange","nodePositionChange","edgeCreate","edgeDelete","nodeDelete"],setup(a,{emit:I}){const r=a,f=I,d=t.ref(r.zoom),x=o=>Math.min(2,Math.max(.4,Number(o.toFixed(2)))),ge=()=>{d.value=x(d.value+.1)},ue=()=>{d.value=x(d.value-.1)};t.watch(()=>r.zoom,o=>{d.value=x(o)});const pe=t.computed(()=>`translate(${r.panX} ${r.panY}) scale(${d.value})`),k=t.ref(),b=t.ref(),y=t.reactive({width:700,height:240});let m;const S=t.computed(()=>r.width??y.width),D=t.computed(()=>r.height??y.height),c=t.reactive({active:!1,nodeId:"",offsetX:0,offsetY:0,latestClientX:0,latestClientY:0}),s=t.reactive({active:!1,sourceNodeId:"",sourcePortId:"",pendingClick:!1,pressedClientX:0,pressedClientY:0,currentX:0,currentY:0}),u=t.reactive({rafId:0,scheduled:!1}),me=o=>{const n=o.data;return(n==null?void 0:n.text)??""},we=o=>{const n=o.data;return(n==null?void 0:n.lineColor)??"#1c7ed6"},ke=o=>{const n=o.data;return(n==null?void 0:n.fontColor)??"#0f172a"},ye=o=>{const n=o.data;return(n==null?void 0:n.lineStyle)==="dashed"?"6 4":void 0},Ie=o=>{const n=o.data;return(n==null?void 0:n.runtimeStatus)??"idle"},xe=o=>Ie(o)==="pending",Ce=o=>{if(o.length<2)return;const n=[];let e=0;for(let l=1;l<o.length;l+=1){const p=o[l-1],z=o[l],B=Math.hypot(z.x-p.x,z.y-p.y);B!==0&&(n.push({start:p,end:z,length:B}),e+=B)}if(n.length===0||e===0)return;const i=e/2;let w=0;for(const l of n){if(w+l.length>=i){const p=(i-w)/l.length;return{x:l.start.x+(l.end.x-l.start.x)*p,y:l.start.y+(l.end.y-l.start.y)*p}}w+=l.length}const W=n[n.length-1];return{x:W.end.x,y:W.end.y}},Ne=t.computed(()=>r.edges.map(o=>{const n=o.points??[],e=M.renderSvgEdge(n,{pixelSnap:!0});return{edge:o,edgeId:o.id,path:e.path,arrow:e.arrow,label:me(o),lineColor:we(o),lineDasharray:ye(o),labelColor:ke(o),labelPoint:Ce(n),flowing:xe(o)}})),Pe=o=>{const n=o.data;return(n==null?void 0:n.title)??o.id},C=o=>{const n=o.data;return(n==null?void 0:n.icon)??""},X=o=>{const n=C(o);return!n||!n.startsWith("icon-")?"":`#${n}`},N=o=>{const n=o.data;return(n==null?void 0:n.runtimeStatus)==="completed"?n.completedFillColor??"#bbf7d0":(n==null?void 0:n.runtimeStatus)==="active"?"#dcfce7":(n==null?void 0:n.nodeColor)??"#ffffff"},Y=o=>{const n=o.data;return(n==null?void 0:n.fontColor)??"#0f172a"},v=o=>{const n=o.data;return(n==null?void 0:n.nodeShape)??"rect"},_e=o=>{const n=o.size.width/2,e=o.size.height/2;return`0,${-e} ${n},0 0,${e} ${-n},0`},Ee=o=>{const n=new Map(o.ports.map(e=>[e.direction,e]));return[{direction:h.PortDirection.Top,dx:0,dy:-o.size.height/2},{direction:h.PortDirection.Right,dx:o.size.width/2,dy:0},{direction:h.PortDirection.Bottom,dx:0,dy:o.size.height/2},{direction:h.PortDirection.Left,dx:-o.size.width/2,dy:0}].map(e=>({...e,port:n.get(e.direction)})).filter(e=>!!e.port)},V=o=>{const n=b.value;if(!n)return{x:o.clientX,y:o.clientY};const e=n.getBoundingClientRect();return{x:o.clientX-e.left,y:o.clientY-e.top}},P=o=>({x:(o.x-r.panX)/d.value,y:(o.y-r.panY)/d.value}),ze=()=>{if(u.scheduled=!1,!c.active)return;const o=r.nodes.find(e=>e.id===c.nodeId);if(!o)return;const n=h.resolveNodeDragPosition({node:o,nodes:r.nodes,alignThreshold:r.alignThreshold,clientX:c.latestClientX,clientY:c.latestClientY,offsetX:c.offsetX,offsetY:c.offsetY,canvasWidth:S.value/d.value,canvasHeight:D.value/d.value,gridSize:r.gridSize,snapToGrid:r.snapToGrid});f("nodePositionChange",{nodeId:c.nodeId,position:n})},Be=()=>{s.active=!1,s.pendingClick=!1,s.sourceNodeId="",s.sourcePortId=""},L=()=>{c.active=!1,c.nodeId="",u.rafId&&(window.cancelAnimationFrame(u.rafId),u.rafId=0),u.scheduled=!1},Me=()=>{if(!s.active&&!s.pendingClick)return;const o=h.resolveLinkSource({sourceNodeId:s.sourceNodeId,sourcePortId:s.sourcePortId,nodes:r.nodes});if(o){const n=h.resolveLinkCompletion({sourceNodeId:o.sourceNode.id,sourcePortId:o.sourcePort.id,sourceNode:o.sourceNode,sourcePort:o.sourcePort,nodes:r.nodes,mode:s.pendingClick?"click":"drag",currentPoint:{x:s.currentX,y:s.currentY},hitRadius:r.portHitRadius});n.targetNodeId&&n.targetPortId&&f("edgeCreate",{sourceNodeId:n.sourceNodeId,sourcePortId:n.sourcePortId,targetNodeId:n.targetNodeId,targetPortId:n.targetPortId})}Be()},R=t.computed(()=>{if(!s.active)return[];const o=h.resolveLinkSource({sourceNodeId:s.sourceNodeId,sourcePortId:s.sourcePortId,nodes:r.nodes});return o?h.buildPreviewRoute({sourceNode:o.sourceNode,sourcePort:o.sourcePort,nodes:r.nodes,currentPoint:{x:s.currentX,y:s.currentY},hitRadius:r.portHitRadius}).points:[]}),T=t.computed(()=>M.renderSvgEdge(R.value,{pixelSnap:!0})),_=(o,n)=>{if(s.active)return;n.preventDefault(),f("selectionChange",{nodeId:o.id}),c.active=!0,c.nodeId=o.id;const e=P(V(n));c.offsetX=e.x-o.position.x,c.offsetY=e.y-o.position.y,c.latestClientX=e.x,c.latestClientY=e.y},be=(o,n,e)=>{e.preventDefault(),f("selectionChange",{nodeId:o.id});const i=h.getPortPoint(o,n);s.active=!1,s.pendingClick=!0,s.pressedClientX=e.clientX,s.pressedClientY=e.clientY,s.sourceNodeId=o.id,s.sourcePortId=n.id,s.currentX=i.x,s.currentY=i.y},E=(o,n)=>{n.preventDefault(),n.stopPropagation(),f("selectionChange",{edgeId:o.id})},Se=()=>{s.active||c.active||f("selectionChange",{})},F=o=>{const n=V(o);if(s.pendingClick&&!s.active&&h.shouldActivateLinkDrag({pressedClientX:s.pressedClientX,pressedClientY:s.pressedClientY,currentClientX:o.clientX,currentClientY:o.clientY,threshold:r.portDragThreshold})&&(s.active=!0,s.pendingClick=!1),s.active){const i=P(n);s.currentX=i.x,s.currentY=i.y;return}if(!c.active)return;const e=P(n);c.latestClientX=e.x,c.latestClientY=e.y,!u.scheduled&&(u.scheduled=!0,u.rafId=window.requestAnimationFrame(ze))},$=()=>{L(),Me()},O=o=>{if(o.key!=="Delete"&&o.key!=="Backspace")return;const n=o.target;if(n){const e=n.tagName;if(e==="INPUT"||e==="TEXTAREA"||n.isContentEditable)return}if(r.selectedEdgeId){o.preventDefault(),f("edgeDelete",{edgeId:r.selectedEdgeId});return}r.selectedNodeId&&(o.preventDefault(),f("nodeDelete",{nodeId:r.selectedNodeId}))};return t.onMounted(()=>{const o=()=>{const n=k.value;if(!n)return;const e=n.getBoundingClientRect();e.width>0&&(y.width=e.width),e.height>0&&(y.height=e.height)};o(),m=new ResizeObserver(o),k.value&&m.observe(k.value),window.addEventListener("mousemove",F),window.addEventListener("mouseup",$),window.addEventListener("keydown",O)}),t.onUnmounted(()=>{L(),m==null||m.disconnect(),window.removeEventListener("mousemove",F),window.removeEventListener("mouseup",$),window.removeEventListener("keydown",O)}),(o,n)=>(t.openBlock(),t.createElementBlock("section",{ref_key:"containerRef",ref:k,class:"canvas-view",style:{width:"100%",height:"100%"}},[t.createElementVNode("div",A,[t.createElementVNode("button",{type:"button",class:"zoom-btn",onClick:ue},"-"),t.createElementVNode("span",H,t.toDisplayString(Math.round(d.value*100))+"%",1),t.createElementVNode("button",{type:"button",class:"zoom-btn",onClick:ge},"+")]),(t.openBlock(),t.createElementBlock("svg",{ref_key:"svgRef",ref:b,class:"canvas-svg",xmlns:"http://www.w3.org/2000/svg",viewBox:`0 0 ${S.value} ${D.value}`,onMousedown:t.withModifiers(Se,["self"])},[t.createElementVNode("g",{transform:pe.value},[t.createElementVNode("g",G,[(t.openBlock(!0),t.createElementBlock(t.Fragment,null,t.renderList(Ne.value,e=>(t.openBlock(),t.createElementBlock(t.Fragment,{key:e.edgeId},[t.createElementVNode("path",{d:e.path,fill:"none",stroke:"transparent","stroke-width":"15","vector-effect":"non-scaling-stroke",class:"edge-hit",onMousedown:t.withModifiers(i=>E(e.edge,i),["stop"])},null,40,U),t.createElementVNode("path",{d:e.path,stroke:a.selectedEdgeId===e.edgeId?"#2563eb":e.lineColor,"stroke-width":"1",fill:"none","stroke-linecap":"round","stroke-linejoin":"round","vector-effect":"non-scaling-stroke","shape-rendering":"geometricPrecision",class:t.normalizeClass(["edge-path",{"edge-flowing":e.flowing}]),"stroke-dasharray":e.lineDasharray??(e.flowing?"7 6":void 0),onMousedown:t.withModifiers(i=>E(e.edge,i),["stop"])},null,42,K),t.createElementVNode("polygon",{points:e.arrow,fill:a.selectedEdgeId===e.edgeId?"#2563eb":e.lineColor,stroke:a.selectedEdgeId===e.edgeId?"#2563eb":e.lineColor,"vector-effect":"non-scaling-stroke","shape-rendering":"geometricPrecision",class:"edge-arrow",onMousedown:t.withModifiers(i=>E(e.edge,i),["stop"])},null,40,Z),e.label&&e.labelPoint?(t.openBlock(),t.createElementBlock("text",{key:0,x:e.labelPoint.x,y:e.labelPoint.y,class:"edge-label",fill:e.labelColor,"text-anchor":"middle","dominant-baseline":"central"},t.toDisplayString(e.label),9,J)):t.createCommentVNode("",!0)],64))),128)),R.value.length>1?(t.openBlock(),t.createElementBlock(t.Fragment,{key:0},[t.createElementVNode("path",{d:T.value.path,stroke:"#94a3b8","stroke-width":"2",fill:"none","stroke-linecap":"round","stroke-linejoin":"round","vector-effect":"non-scaling-stroke","shape-rendering":"geometricPrecision"},null,8,Q),t.createElementVNode("polygon",{points:T.value.arrow,fill:"#94a3b8",stroke:"#94a3b8","vector-effect":"non-scaling-stroke","shape-rendering":"geometricPrecision"},null,8,ee)],64)):t.createCommentVNode("",!0)]),t.createElementVNode("g",te,[(t.openBlock(!0),t.createElementBlock(t.Fragment,null,t.renderList(a.nodes,e=>(t.openBlock(),t.createElementBlock("g",{key:e.id,class:"node-group",transform:`translate(${e.position.x+e.size.width/2} ${e.position.y+e.size.height/2})`},[v(e)==="rect"?(t.openBlock(),t.createElementBlock("rect",{key:0,x:-e.size.width/2,y:-e.size.height/2,width:e.size.width,height:e.size.height,rx:"4",ry:"4",fill:N(e),stroke:"#0f172a",class:t.normalizeClass({selected:a.selectedNodeId===e.id}),onMousedown:t.withModifiers(i=>_(e,i),["stop"])},null,42,ne)):v(e)==="ellipse"?(t.openBlock(),t.createElementBlock("rect",{key:1,x:-e.size.width/2,y:-e.size.height/2,width:e.size.width,height:e.size.height,rx:e.size.height/2,ry:e.size.height/2,fill:N(e),stroke:"#0f172a",class:t.normalizeClass({selected:a.selectedNodeId===e.id}),onMousedown:t.withModifiers(i=>_(e,i),["stop"])},null,42,se)):(t.openBlock(),t.createElementBlock("polygon",{key:2,points:_e(e),fill:N(e),stroke:"#0f172a",class:t.normalizeClass({selected:a.selectedNodeId===e.id}),onMousedown:t.withModifiers(i=>_(e,i),["stop"])},null,42,re)),(t.openBlock(),t.createElementBlock("foreignObject",{x:-e.size.width/2,y:-e.size.height/2,width:e.size.width,height:e.size.height,class:"node-title-foreign"},[t.createElementVNode("div",{xmlns:"http://www.w3.org/1999/xhtml",class:"node-title-dom",style:t.normalizeStyle({color:Y(e)})},[X(e)?(t.openBlock(),t.createElementBlock("svg",ce,[t.createElementVNode("use",{href:X(e),fill:Y(e)},null,8,le)])):C(e)?(t.openBlock(),t.createElementBlock("span",de,t.toDisplayString(C(e)),1)):t.createCommentVNode("",!0),t.createElementVNode("span",ae,t.toDisplayString(Pe(e)),1)],4)],8,ie)),(t.openBlock(!0),t.createElementBlock(t.Fragment,null,t.renderList(Ee(e),i=>t.withDirectives((t.openBlock(),t.createElementBlock("circle",{key:i.port.id,cx:i.dx,cy:i.dy,r:"5",fill:"#ffffff",stroke:"#1d4ed8",class:"port-dot",onMousedown:t.withModifiers(w=>be(e,i.port,w),["stop"])},null,40,he)),[[t.vShow,a.selectedNodeId===e.id]])),128))],8,oe))),128))])],8,q)],40,j))],512))}}),[["__scopeId","data-v-72a1e4d3"]]);g.CanvasView=fe,Object.defineProperty(g,Symbol.toStringTag,{value:"Module"})});
@@ -0,0 +1,3 @@
1
+ import { DefineComponent } from 'vue'
2
+
3
+ export declare const CanvasView: DefineComponent<{}, {}, any>
package/dist/index.mjs ADDED
@@ -0,0 +1,457 @@
1
+ import { defineComponent as ke, ref as R, watch as me, computed as w, reactive as x, onMounted as Ce, onUnmounted as xe, openBlock as d, createElementBlock as l, createElementVNode as c, toDisplayString as P, withModifiers as p, Fragment as k, renderList as T, normalizeClass as _, createCommentVNode as $, normalizeStyle as Pe, withDirectives as _e, vShow as ze } from "vue";
2
+ import { resolveLinkSource as Q, buildPreviewRoute as Ne, PortDirection as z, getPortPoint as be, shouldActivateLinkDrag as Me, resolveNodeDragPosition as Xe, resolveLinkCompletion as Ee } from "@michael_home/workflow-engine-core";
3
+ import { renderSvgEdge as ee } from "@michael_home/workflow-engine-svg-renderer";
4
+ const Ye = { class: "zoom-controls" }, De = { class: "zoom-text" }, Se = ["viewBox"], Le = ["transform"], Re = { class: "edge-layer" }, Te = ["d", "onMousedown"], $e = ["d", "stroke", "stroke-dasharray", "onMousedown"], Be = ["points", "fill", "stroke", "onMousedown"], Fe = ["x", "y", "fill"], Oe = ["d"], Ae = ["points"], He = { class: "node-layer" }, We = ["transform"], Ve = ["x", "y", "width", "height", "fill", "onMousedown"], je = ["x", "y", "width", "height", "rx", "ry", "fill", "onMousedown"], Ge = ["points", "fill", "onMousedown"], Ue = ["x", "y", "width", "height"], qe = {
5
+ key: 0,
6
+ width: "14",
7
+ height: "14",
8
+ class: "node-icon-dom",
9
+ "aria-hidden": "true"
10
+ }, Ke = ["href", "fill"], Ze = {
11
+ key: 1,
12
+ class: "node-icon-fallback"
13
+ }, Je = { class: "node-title-text" }, Qe = ["cx", "cy", "onMousedown"], et = /* @__PURE__ */ ke({
14
+ __name: "CanvasView",
15
+ props: {
16
+ nodes: {},
17
+ edges: {},
18
+ selectedNodeId: {},
19
+ selectedEdgeId: {},
20
+ width: {},
21
+ height: {},
22
+ panX: { default: 0 },
23
+ panY: { default: 0 },
24
+ zoom: { default: 1 },
25
+ snapToGrid: { type: Boolean, default: !0 },
26
+ gridSize: { default: 10 },
27
+ alignThreshold: { default: 10 },
28
+ portHitRadius: { default: 14 },
29
+ portDragThreshold: { default: 4 }
30
+ },
31
+ emits: ["selectionChange", "nodePositionChange", "edgeCreate", "edgeDelete", "nodeDelete"],
32
+ setup(h, { emit: N }) {
33
+ const s = h, f = N, u = R(s.zoom), b = (t) => Math.min(2, Math.max(0.4, Number(t.toFixed(2)))), te = () => {
34
+ u.value = b(u.value + 0.1);
35
+ }, oe = () => {
36
+ u.value = b(u.value - 0.1);
37
+ };
38
+ me(
39
+ () => s.zoom,
40
+ (t) => {
41
+ u.value = b(t);
42
+ }
43
+ );
44
+ const ne = w(
45
+ () => `translate(${s.panX} ${s.panY}) scale(${u.value})`
46
+ ), m = R(), B = R(), C = x({ width: 700, height: 240 });
47
+ let y;
48
+ const F = w(() => s.width ?? C.width), O = w(() => s.height ?? C.height), i = x({
49
+ active: !1,
50
+ nodeId: "",
51
+ offsetX: 0,
52
+ offsetY: 0,
53
+ latestClientX: 0,
54
+ latestClientY: 0
55
+ }), n = x({
56
+ active: !1,
57
+ sourceNodeId: "",
58
+ sourcePortId: "",
59
+ pendingClick: !1,
60
+ pressedClientX: 0,
61
+ pressedClientY: 0,
62
+ currentX: 0,
63
+ currentY: 0
64
+ }), g = x({ rafId: 0, scheduled: !1 }), se = (t) => {
65
+ const o = t.data;
66
+ return (o == null ? void 0 : o.text) ?? "";
67
+ }, re = (t) => {
68
+ const o = t.data;
69
+ return (o == null ? void 0 : o.lineColor) ?? "#1c7ed6";
70
+ }, ie = (t) => {
71
+ const o = t.data;
72
+ return (o == null ? void 0 : o.fontColor) ?? "#0f172a";
73
+ }, de = (t) => {
74
+ const o = t.data;
75
+ return (o == null ? void 0 : o.lineStyle) === "dashed" ? "6 4" : void 0;
76
+ }, le = (t) => {
77
+ const o = t.data;
78
+ return (o == null ? void 0 : o.runtimeStatus) ?? "idle";
79
+ }, ce = (t) => le(t) === "pending", ae = (t) => {
80
+ if (t.length < 2) return;
81
+ const o = [];
82
+ let e = 0;
83
+ for (let a = 1; a < t.length; a += 1) {
84
+ const v = t[a - 1], S = t[a], L = Math.hypot(S.x - v.x, S.y - v.y);
85
+ L !== 0 && (o.push({ start: v, end: S, length: L }), e += L);
86
+ }
87
+ if (o.length === 0 || e === 0) return;
88
+ const r = e / 2;
89
+ let I = 0;
90
+ for (const a of o) {
91
+ if (I + a.length >= r) {
92
+ const v = (r - I) / a.length;
93
+ return {
94
+ x: a.start.x + (a.end.x - a.start.x) * v,
95
+ y: a.start.y + (a.end.y - a.start.y) * v
96
+ };
97
+ }
98
+ I += a.length;
99
+ }
100
+ const J = o[o.length - 1];
101
+ return { x: J.end.x, y: J.end.y };
102
+ }, ue = w(
103
+ () => s.edges.map((t) => {
104
+ const o = t.points ?? [], e = ee(o, { pixelSnap: !0 });
105
+ return {
106
+ edge: t,
107
+ edgeId: t.id,
108
+ path: e.path,
109
+ arrow: e.arrow,
110
+ label: se(t),
111
+ lineColor: re(t),
112
+ lineDasharray: de(t),
113
+ labelColor: ie(t),
114
+ labelPoint: ae(o),
115
+ flowing: ce(t)
116
+ };
117
+ })
118
+ ), he = (t) => {
119
+ const o = t.data;
120
+ return (o == null ? void 0 : o.title) ?? t.id;
121
+ }, M = (t) => {
122
+ const o = t.data;
123
+ return (o == null ? void 0 : o.icon) ?? "";
124
+ }, A = (t) => {
125
+ const o = M(t);
126
+ return !o || !o.startsWith("icon-") ? "" : `#${o}`;
127
+ }, X = (t) => {
128
+ const o = t.data;
129
+ return (o == null ? void 0 : o.runtimeStatus) === "completed" ? o.completedFillColor ?? "#bbf7d0" : (o == null ? void 0 : o.runtimeStatus) === "active" ? "#dcfce7" : (o == null ? void 0 : o.nodeColor) ?? "#ffffff";
130
+ }, H = (t) => {
131
+ const o = t.data;
132
+ return (o == null ? void 0 : o.fontColor) ?? "#0f172a";
133
+ }, W = (t) => {
134
+ const o = t.data;
135
+ return (o == null ? void 0 : o.nodeShape) ?? "rect";
136
+ }, fe = (t) => {
137
+ const o = t.size.width / 2, e = t.size.height / 2;
138
+ return `0,${-e} ${o},0 0,${e} ${-o},0`;
139
+ }, ge = (t) => {
140
+ const o = new Map(t.ports.map((e) => [e.direction, e]));
141
+ return [
142
+ { direction: z.Top, dx: 0, dy: -t.size.height / 2 },
143
+ { direction: z.Right, dx: t.size.width / 2, dy: 0 },
144
+ { direction: z.Bottom, dx: 0, dy: t.size.height / 2 },
145
+ { direction: z.Left, dx: -t.size.width / 2, dy: 0 }
146
+ ].map((e) => ({
147
+ ...e,
148
+ port: o.get(e.direction)
149
+ })).filter((e) => !!e.port);
150
+ }, V = (t) => {
151
+ const o = B.value;
152
+ if (!o)
153
+ return { x: t.clientX, y: t.clientY };
154
+ const e = o.getBoundingClientRect();
155
+ return {
156
+ x: t.clientX - e.left,
157
+ y: t.clientY - e.top
158
+ };
159
+ }, E = (t) => ({
160
+ x: (t.x - s.panX) / u.value,
161
+ y: (t.y - s.panY) / u.value
162
+ }), pe = () => {
163
+ if (g.scheduled = !1, !i.active) return;
164
+ const t = s.nodes.find((e) => e.id === i.nodeId);
165
+ if (!t) return;
166
+ const o = Xe({
167
+ node: t,
168
+ nodes: s.nodes,
169
+ alignThreshold: s.alignThreshold,
170
+ clientX: i.latestClientX,
171
+ clientY: i.latestClientY,
172
+ offsetX: i.offsetX,
173
+ offsetY: i.offsetY,
174
+ canvasWidth: F.value / u.value,
175
+ canvasHeight: O.value / u.value,
176
+ gridSize: s.gridSize,
177
+ snapToGrid: s.snapToGrid
178
+ });
179
+ f("nodePositionChange", { nodeId: i.nodeId, position: o });
180
+ }, ve = () => {
181
+ n.active = !1, n.pendingClick = !1, n.sourceNodeId = "", n.sourcePortId = "";
182
+ }, j = () => {
183
+ i.active = !1, i.nodeId = "", g.rafId && (window.cancelAnimationFrame(g.rafId), g.rafId = 0), g.scheduled = !1;
184
+ }, we = () => {
185
+ if (!n.active && !n.pendingClick) return;
186
+ const t = Q({
187
+ sourceNodeId: n.sourceNodeId,
188
+ sourcePortId: n.sourcePortId,
189
+ nodes: s.nodes
190
+ });
191
+ if (t) {
192
+ const o = Ee({
193
+ sourceNodeId: t.sourceNode.id,
194
+ sourcePortId: t.sourcePort.id,
195
+ sourceNode: t.sourceNode,
196
+ sourcePort: t.sourcePort,
197
+ nodes: s.nodes,
198
+ mode: n.pendingClick ? "click" : "drag",
199
+ currentPoint: { x: n.currentX, y: n.currentY },
200
+ hitRadius: s.portHitRadius
201
+ });
202
+ o.targetNodeId && o.targetPortId && f("edgeCreate", {
203
+ sourceNodeId: o.sourceNodeId,
204
+ sourcePortId: o.sourcePortId,
205
+ targetNodeId: o.targetNodeId,
206
+ targetPortId: o.targetPortId
207
+ });
208
+ }
209
+ ve();
210
+ }, G = w(() => {
211
+ if (!n.active) return [];
212
+ const t = Q({
213
+ sourceNodeId: n.sourceNodeId,
214
+ sourcePortId: n.sourcePortId,
215
+ nodes: s.nodes
216
+ });
217
+ return t ? Ne({
218
+ sourceNode: t.sourceNode,
219
+ sourcePort: t.sourcePort,
220
+ nodes: s.nodes,
221
+ currentPoint: { x: n.currentX, y: n.currentY },
222
+ hitRadius: s.portHitRadius
223
+ }).points : [];
224
+ }), U = w(() => ee(G.value, { pixelSnap: !0 })), Y = (t, o) => {
225
+ if (n.active) return;
226
+ o.preventDefault(), f("selectionChange", { nodeId: t.id }), i.active = !0, i.nodeId = t.id;
227
+ const e = E(V(o));
228
+ i.offsetX = e.x - t.position.x, i.offsetY = e.y - t.position.y, i.latestClientX = e.x, i.latestClientY = e.y;
229
+ }, ye = (t, o, e) => {
230
+ e.preventDefault(), f("selectionChange", { nodeId: t.id });
231
+ const r = be(t, o);
232
+ n.active = !1, n.pendingClick = !0, n.pressedClientX = e.clientX, n.pressedClientY = e.clientY, n.sourceNodeId = t.id, n.sourcePortId = o.id, n.currentX = r.x, n.currentY = r.y;
233
+ }, D = (t, o) => {
234
+ o.preventDefault(), o.stopPropagation(), f("selectionChange", { edgeId: t.id });
235
+ }, Ie = () => {
236
+ n.active || i.active || f("selectionChange", {});
237
+ }, q = (t) => {
238
+ const o = V(t);
239
+ if (n.pendingClick && !n.active && Me({
240
+ pressedClientX: n.pressedClientX,
241
+ pressedClientY: n.pressedClientY,
242
+ currentClientX: t.clientX,
243
+ currentClientY: t.clientY,
244
+ threshold: s.portDragThreshold
245
+ }) && (n.active = !0, n.pendingClick = !1), n.active) {
246
+ const r = E(o);
247
+ n.currentX = r.x, n.currentY = r.y;
248
+ return;
249
+ }
250
+ if (!i.active) return;
251
+ const e = E(o);
252
+ i.latestClientX = e.x, i.latestClientY = e.y, !g.scheduled && (g.scheduled = !0, g.rafId = window.requestAnimationFrame(pe));
253
+ }, K = () => {
254
+ j(), we();
255
+ }, Z = (t) => {
256
+ if (t.key !== "Delete" && t.key !== "Backspace") return;
257
+ const o = t.target;
258
+ if (o) {
259
+ const e = o.tagName;
260
+ if (e === "INPUT" || e === "TEXTAREA" || o.isContentEditable)
261
+ return;
262
+ }
263
+ if (s.selectedEdgeId) {
264
+ t.preventDefault(), f("edgeDelete", { edgeId: s.selectedEdgeId });
265
+ return;
266
+ }
267
+ s.selectedNodeId && (t.preventDefault(), f("nodeDelete", { nodeId: s.selectedNodeId }));
268
+ };
269
+ return Ce(() => {
270
+ const t = () => {
271
+ const o = m.value;
272
+ if (!o) return;
273
+ const e = o.getBoundingClientRect();
274
+ e.width > 0 && (C.width = e.width), e.height > 0 && (C.height = e.height);
275
+ };
276
+ t(), y = new ResizeObserver(t), m.value && y.observe(m.value), window.addEventListener("mousemove", q), window.addEventListener("mouseup", K), window.addEventListener("keydown", Z);
277
+ }), xe(() => {
278
+ j(), y == null || y.disconnect(), window.removeEventListener("mousemove", q), window.removeEventListener("mouseup", K), window.removeEventListener("keydown", Z);
279
+ }), (t, o) => (d(), l("section", {
280
+ ref_key: "containerRef",
281
+ ref: m,
282
+ class: "canvas-view",
283
+ style: { width: "100%", height: "100%" }
284
+ }, [
285
+ c("div", Ye, [
286
+ c("button", {
287
+ type: "button",
288
+ class: "zoom-btn",
289
+ onClick: oe
290
+ }, "-"),
291
+ c("span", De, P(Math.round(u.value * 100)) + "%", 1),
292
+ c("button", {
293
+ type: "button",
294
+ class: "zoom-btn",
295
+ onClick: te
296
+ }, "+")
297
+ ]),
298
+ (d(), l("svg", {
299
+ ref_key: "svgRef",
300
+ ref: B,
301
+ class: "canvas-svg",
302
+ xmlns: "http://www.w3.org/2000/svg",
303
+ viewBox: `0 0 ${F.value} ${O.value}`,
304
+ onMousedown: p(Ie, ["self"])
305
+ }, [
306
+ c("g", { transform: ne.value }, [
307
+ c("g", Re, [
308
+ (d(!0), l(k, null, T(ue.value, (e) => (d(), l(k, {
309
+ key: e.edgeId
310
+ }, [
311
+ c("path", {
312
+ d: e.path,
313
+ fill: "none",
314
+ stroke: "transparent",
315
+ "stroke-width": "15",
316
+ "vector-effect": "non-scaling-stroke",
317
+ class: "edge-hit",
318
+ onMousedown: p((r) => D(e.edge, r), ["stop"])
319
+ }, null, 40, Te),
320
+ c("path", {
321
+ d: e.path,
322
+ stroke: h.selectedEdgeId === e.edgeId ? "#2563eb" : e.lineColor,
323
+ "stroke-width": "1",
324
+ fill: "none",
325
+ "stroke-linecap": "round",
326
+ "stroke-linejoin": "round",
327
+ "vector-effect": "non-scaling-stroke",
328
+ "shape-rendering": "geometricPrecision",
329
+ class: _(["edge-path", { "edge-flowing": e.flowing }]),
330
+ "stroke-dasharray": e.lineDasharray ?? (e.flowing ? "7 6" : void 0),
331
+ onMousedown: p((r) => D(e.edge, r), ["stop"])
332
+ }, null, 42, $e),
333
+ c("polygon", {
334
+ points: e.arrow,
335
+ fill: h.selectedEdgeId === e.edgeId ? "#2563eb" : e.lineColor,
336
+ stroke: h.selectedEdgeId === e.edgeId ? "#2563eb" : e.lineColor,
337
+ "vector-effect": "non-scaling-stroke",
338
+ "shape-rendering": "geometricPrecision",
339
+ class: "edge-arrow",
340
+ onMousedown: p((r) => D(e.edge, r), ["stop"])
341
+ }, null, 40, Be),
342
+ e.label && e.labelPoint ? (d(), l("text", {
343
+ key: 0,
344
+ x: e.labelPoint.x,
345
+ y: e.labelPoint.y,
346
+ class: "edge-label",
347
+ fill: e.labelColor,
348
+ "text-anchor": "middle",
349
+ "dominant-baseline": "central"
350
+ }, P(e.label), 9, Fe)) : $("", !0)
351
+ ], 64))), 128)),
352
+ G.value.length > 1 ? (d(), l(k, { key: 0 }, [
353
+ c("path", {
354
+ d: U.value.path,
355
+ stroke: "#94a3b8",
356
+ "stroke-width": "2",
357
+ fill: "none",
358
+ "stroke-linecap": "round",
359
+ "stroke-linejoin": "round",
360
+ "vector-effect": "non-scaling-stroke",
361
+ "shape-rendering": "geometricPrecision"
362
+ }, null, 8, Oe),
363
+ c("polygon", {
364
+ points: U.value.arrow,
365
+ fill: "#94a3b8",
366
+ stroke: "#94a3b8",
367
+ "vector-effect": "non-scaling-stroke",
368
+ "shape-rendering": "geometricPrecision"
369
+ }, null, 8, Ae)
370
+ ], 64)) : $("", !0)
371
+ ]),
372
+ c("g", He, [
373
+ (d(!0), l(k, null, T(h.nodes, (e) => (d(), l("g", {
374
+ key: e.id,
375
+ class: "node-group",
376
+ transform: `translate(${e.position.x + e.size.width / 2} ${e.position.y + e.size.height / 2})`
377
+ }, [
378
+ W(e) === "rect" ? (d(), l("rect", {
379
+ key: 0,
380
+ x: -e.size.width / 2,
381
+ y: -e.size.height / 2,
382
+ width: e.size.width,
383
+ height: e.size.height,
384
+ rx: "4",
385
+ ry: "4",
386
+ fill: X(e),
387
+ stroke: "#0f172a",
388
+ class: _({ selected: h.selectedNodeId === e.id }),
389
+ onMousedown: p((r) => Y(e, r), ["stop"])
390
+ }, null, 42, Ve)) : W(e) === "ellipse" ? (d(), l("rect", {
391
+ key: 1,
392
+ x: -e.size.width / 2,
393
+ y: -e.size.height / 2,
394
+ width: e.size.width,
395
+ height: e.size.height,
396
+ rx: e.size.height / 2,
397
+ ry: e.size.height / 2,
398
+ fill: X(e),
399
+ stroke: "#0f172a",
400
+ class: _({ selected: h.selectedNodeId === e.id }),
401
+ onMousedown: p((r) => Y(e, r), ["stop"])
402
+ }, null, 42, je)) : (d(), l("polygon", {
403
+ key: 2,
404
+ points: fe(e),
405
+ fill: X(e),
406
+ stroke: "#0f172a",
407
+ class: _({ selected: h.selectedNodeId === e.id }),
408
+ onMousedown: p((r) => Y(e, r), ["stop"])
409
+ }, null, 42, Ge)),
410
+ (d(), l("foreignObject", {
411
+ x: -e.size.width / 2,
412
+ y: -e.size.height / 2,
413
+ width: e.size.width,
414
+ height: e.size.height,
415
+ class: "node-title-foreign"
416
+ }, [
417
+ c("div", {
418
+ xmlns: "http://www.w3.org/1999/xhtml",
419
+ class: "node-title-dom",
420
+ style: Pe({ color: H(e) })
421
+ }, [
422
+ A(e) ? (d(), l("svg", qe, [
423
+ c("use", {
424
+ href: A(e),
425
+ fill: H(e)
426
+ }, null, 8, Ke)
427
+ ])) : M(e) ? (d(), l("span", Ze, P(M(e)), 1)) : $("", !0),
428
+ c("span", Je, P(he(e)), 1)
429
+ ], 4)
430
+ ], 8, Ue)),
431
+ (d(!0), l(k, null, T(ge(e), (r) => _e((d(), l("circle", {
432
+ key: r.port.id,
433
+ cx: r.dx,
434
+ cy: r.dy,
435
+ r: "5",
436
+ fill: "#ffffff",
437
+ stroke: "#1d4ed8",
438
+ class: "port-dot",
439
+ onMousedown: p((I) => ye(e, r.port, I), ["stop"])
440
+ }, null, 40, Qe)), [
441
+ [ze, h.selectedNodeId === e.id]
442
+ ])), 128))
443
+ ], 8, We))), 128))
444
+ ])
445
+ ], 8, Le)
446
+ ], 40, Se))
447
+ ], 512));
448
+ }
449
+ }), tt = (h, N) => {
450
+ const s = h.__vccOpts || h;
451
+ for (const [f, u] of N)
452
+ s[f] = u;
453
+ return s;
454
+ }, rt = /* @__PURE__ */ tt(et, [["__scopeId", "data-v-72a1e4d3"]]);
455
+ export {
456
+ rt as CanvasView
457
+ };
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ .canvas-view[data-v-72a1e4d3]{position:relative;overflow:hidden;border:1px solid #dee2e6;background:#f8f9fa}.zoom-controls[data-v-72a1e4d3]{position:absolute;top:8px;right:8px;z-index:2;display:inline-flex;align-items:center;gap:4px;background:#fffffff2;border:1px solid #cbd5e1;border-radius:6px;padding:4px 6px}.zoom-btn[data-v-72a1e4d3]{width:24px;height:24px;border:1px solid #cbd5e1;border-radius:4px;background:#f8fafc;cursor:pointer;line-height:1}.zoom-text[data-v-72a1e4d3]{min-width:44px;text-align:center;font-size:12px;color:#334155}.canvas-svg[data-v-72a1e4d3]{width:100%;height:100%}rect.selected[data-v-72a1e4d3],polygon.selected[data-v-72a1e4d3]{stroke:#2563eb;stroke-width:2}.port-dot[data-v-72a1e4d3]{cursor:crosshair}.edge-path[data-v-72a1e4d3],.edge-arrow[data-v-72a1e4d3]{cursor:pointer}.edge-flowing[data-v-72a1e4d3]{animation:edge-flow-72a1e4d3 .8s linear infinite}@keyframes edge-flow-72a1e4d3{0%{stroke-dashoffset:0}to{stroke-dashoffset:-13}}.edge-label[data-v-72a1e4d3]{font-size:12px;pointer-events:none;-webkit-user-select:none;user-select:none}.node-title-foreign[data-v-72a1e4d3]{pointer-events:none}.node-title-dom[data-v-72a1e4d3]{width:100%;height:100%;display:flex;align-items:center;justify-content:center;gap:6px;font-size:13px;-webkit-user-select:none;user-select:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.node-icon-dom[data-v-72a1e4d3],.node-icon-fallback[data-v-72a1e4d3]{flex:0 0 auto}.node-title-text[data-v-72a1e4d3]{overflow:hidden;text-overflow:ellipsis}
package/package.json CHANGED
@@ -1,18 +1,20 @@
1
1
  {
2
2
  "name": "@michael_home/workflow-engine-vue",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Vue 3 components for workflow engine canvas",
5
5
  "type": "module",
6
- "main": "./src/index.ts",
7
- "types": "./src/index.ts",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
8
9
  "exports": {
9
10
  ".": {
10
- "import": "./src/index.ts",
11
- "types": "./src/index.ts"
11
+ "require": "./dist/index.cjs",
12
+ "import": "./dist/index.mjs",
13
+ "types": "./dist/index.d.ts"
12
14
  }
13
15
  },
14
16
  "files": [
15
- "src"
17
+ "dist"
16
18
  ],
17
19
  "keywords": [
18
20
  "workflow",
@@ -36,8 +38,8 @@
36
38
  "vue": "^3.5.0"
37
39
  },
38
40
  "scripts": {
39
- "build": "echo 'Vue package uses source files directly'",
40
- "check": "echo 'Vue package skips type checking'",
41
- "clean": "echo 'Nothing to clean'"
41
+ "build": "vite build",
42
+ "check": "echo 'Vue package type checking via vite'",
43
+ "clean": "rm -rf dist"
42
44
  }
43
45
  }
@@ -1,759 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
3
- import {
4
- PortDirection,
5
- buildPreviewRoute,
6
- getPortPoint,
7
- resolveLinkCompletion,
8
- resolveLinkSource,
9
- resolveNodeDragPosition,
10
- shouldActivateLinkDrag
11
- } from '@michael_home/workflow-engine-core'
12
- import type { GraphEdge, GraphNode, Point, Port } from '@michael_home/workflow-engine-core'
13
- import { renderSvgEdge } from '@michael_home/workflow-engine-svg-renderer'
14
-
15
- const props = withDefaults(
16
- defineProps<{
17
- nodes: GraphNode<unknown>[]
18
- edges: GraphEdge<unknown>[]
19
- selectedNodeId?: string
20
- selectedEdgeId?: string
21
- width?: number
22
- height?: number
23
- panX?: number
24
- panY?: number
25
- zoom?: number
26
- snapToGrid?: boolean
27
- gridSize?: number
28
- alignThreshold?: number
29
- portHitRadius?: number
30
- portDragThreshold?: number
31
- }>(),
32
- {
33
- panX: 0,
34
- panY: 0,
35
- zoom: 1,
36
- snapToGrid: true,
37
- gridSize: 10,
38
- alignThreshold: 10,
39
- portHitRadius: 14,
40
- portDragThreshold: 4
41
- }
42
- )
43
-
44
- const emit = defineEmits<{
45
- selectionChange: [selection: { nodeId?: string; edgeId?: string }]
46
- nodePositionChange: [payload: { nodeId: string; position: Point }]
47
- edgeCreate: [payload: { sourceNodeId: string; sourcePortId: string; targetNodeId: string; targetPortId: string }]
48
- edgeDelete: [payload: { edgeId: string }]
49
- nodeDelete: [payload: { nodeId: string }]
50
- }>()
51
-
52
- const zoomLevel = ref(props.zoom)
53
-
54
- const clampZoom = (value: number) => Math.min(2, Math.max(0.4, Number(value.toFixed(2))))
55
-
56
- const zoomIn = () => {
57
- zoomLevel.value = clampZoom(zoomLevel.value + 0.1)
58
- }
59
-
60
- const zoomOut = () => {
61
- zoomLevel.value = clampZoom(zoomLevel.value - 0.1)
62
- }
63
-
64
- watch(
65
- () => props.zoom,
66
- (value) => {
67
- zoomLevel.value = clampZoom(value)
68
- }
69
- )
70
-
71
- const viewportTransform = computed(
72
- () => `translate(${props.panX} ${props.panY}) scale(${zoomLevel.value})`
73
- )
74
-
75
- const containerRef = ref<HTMLElement>()
76
- const svgRef = ref<SVGSVGElement>()
77
- const sizeState = reactive({ width: 700, height: 240 })
78
- let resizeObserver: ResizeObserver | undefined
79
-
80
- const canvasWidth = computed(() => props.width ?? sizeState.width)
81
- const canvasHeight = computed(() => props.height ?? sizeState.height)
82
-
83
- const dragState = reactive({
84
- active: false,
85
- nodeId: '',
86
- offsetX: 0,
87
- offsetY: 0,
88
- latestClientX: 0,
89
- latestClientY: 0
90
- })
91
-
92
- const linkState = reactive({
93
- active: false,
94
- sourceNodeId: '',
95
- sourcePortId: '',
96
- pendingClick: false,
97
- pressedClientX: 0,
98
- pressedClientY: 0,
99
- currentX: 0,
100
- currentY: 0
101
- })
102
-
103
- const frameState = reactive({ rafId: 0, scheduled: false })
104
-
105
- const resolveEdgeLabel = (edge: GraphEdge<unknown>): string => {
106
- const data = edge.data as { text?: string } | undefined
107
- return data?.text ?? ''
108
- }
109
-
110
- const resolveEdgeLineColor = (edge: GraphEdge<unknown>): string => {
111
- const data = edge.data as { lineColor?: string } | undefined
112
- return data?.lineColor ?? '#1c7ed6'
113
- }
114
-
115
- const resolveEdgeFontColor = (edge: GraphEdge<unknown>): string => {
116
- const data = edge.data as { fontColor?: string } | undefined
117
- return data?.fontColor ?? '#0f172a'
118
- }
119
-
120
- const resolveEdgeLineDasharray = (edge: GraphEdge<unknown>): string | undefined => {
121
- const data = edge.data as { lineStyle?: 'solid' | 'dashed' } | undefined
122
- return data?.lineStyle === 'dashed' ? '6 4' : undefined
123
- }
124
-
125
- const edgeRuntimeStatusOf = (edge: GraphEdge<unknown>): 'idle' | 'pending' | 'completed' | 'disabled' => {
126
- const data = edge.data as { runtimeStatus?: 'idle' | 'pending' | 'completed' | 'disabled' } | undefined
127
- return data?.runtimeStatus ?? 'idle'
128
- }
129
-
130
- const isFlowingEdge = (edge: GraphEdge<unknown>): boolean => edgeRuntimeStatusOf(edge) === 'pending'
131
-
132
- const getEdgeLabelPoint = (points: Point[]): Point | undefined => {
133
- if (points.length < 2) return undefined
134
-
135
- const segments: Array<{ start: Point; end: Point; length: number }> = []
136
- let totalLength = 0
137
-
138
- for (let i = 1; i < points.length; i += 1) {
139
- const start = points[i - 1]
140
- const end = points[i]
141
- const length = Math.hypot(end.x - start.x, end.y - start.y)
142
- if (length === 0) continue
143
- segments.push({ start, end, length })
144
- totalLength += length
145
- }
146
-
147
- if (segments.length === 0 || totalLength === 0) return undefined
148
-
149
- const half = totalLength / 2
150
- let acc = 0
151
-
152
- for (const segment of segments) {
153
- if (acc + segment.length >= half) {
154
- const ratio = (half - acc) / segment.length
155
- return {
156
- x: segment.start.x + (segment.end.x - segment.start.x) * ratio,
157
- y: segment.start.y + (segment.end.y - segment.start.y) * ratio
158
- }
159
- }
160
- acc += segment.length
161
- }
162
-
163
- const last = segments[segments.length - 1]
164
- return { x: last.end.x, y: last.end.y }
165
- }
166
-
167
- const renderedEdges = computed(() =>
168
- props.edges.map((edge) => {
169
- const points: Point[] = edge.points ?? []
170
- const render = renderSvgEdge(points, { pixelSnap: true })
171
- return {
172
- edge,
173
- edgeId: edge.id,
174
- path: render.path,
175
- arrow: render.arrow,
176
- label: resolveEdgeLabel(edge),
177
- lineColor: resolveEdgeLineColor(edge),
178
- lineDasharray: resolveEdgeLineDasharray(edge),
179
- labelColor: resolveEdgeFontColor(edge),
180
- labelPoint: getEdgeLabelPoint(points),
181
- flowing: isFlowingEdge(edge)
182
- }
183
- })
184
- )
185
-
186
- const titleOf = (node: GraphNode<unknown>): string => {
187
- const data = node.data as { title?: string } | undefined
188
- return data?.title ?? node.id
189
- }
190
-
191
- const iconOf = (node: GraphNode<unknown>): string => {
192
- const data = node.data as { icon?: string } | undefined
193
- return data?.icon ?? ''
194
- }
195
-
196
- const iconSymbolHrefOf = (node: GraphNode<unknown>): string => {
197
- const icon = iconOf(node)
198
- if (!icon || !icon.startsWith('icon-')) return ''
199
- return `#${icon}`
200
- }
201
-
202
- const resolveNodeFillColor = (node: GraphNode<unknown>): string => {
203
- const data = node.data as
204
- | { nodeColor?: string; runtimeStatus?: 'idle' | 'active' | 'completed'; completedFillColor?: string }
205
- | undefined
206
- if (data?.runtimeStatus === 'completed') {
207
- return data.completedFillColor ?? '#bbf7d0'
208
- }
209
- if (data?.runtimeStatus === 'active') {
210
- return '#dcfce7'
211
- }
212
- return data?.nodeColor ?? '#ffffff'
213
- }
214
-
215
- const resolveNodeFontColor = (node: GraphNode<unknown>): string => {
216
- const data = node.data as { fontColor?: string } | undefined
217
- return data?.fontColor ?? '#0f172a'
218
- }
219
-
220
- const resolveNodeShape = (node: GraphNode<unknown>): 'rect' | 'ellipse' | 'diamond' => {
221
- const data = node.data as { nodeShape?: 'rect' | 'ellipse' | 'diamond' } | undefined
222
- return data?.nodeShape ?? 'rect'
223
- }
224
-
225
- const diamondPointsOf = (node: GraphNode<unknown>): string => {
226
- const halfW = node.size.width / 2
227
- const halfH = node.size.height / 2
228
- return `0,${-halfH} ${halfW},0 0,${halfH} ${-halfW},0`
229
- }
230
-
231
- const portsOf = (node: GraphNode<unknown>) => {
232
- const byDirection = new Map(node.ports.map((port) => [port.direction, port]))
233
-
234
- return [
235
- { direction: PortDirection.Top, dx: 0, dy: -node.size.height / 2 },
236
- { direction: PortDirection.Right, dx: node.size.width / 2, dy: 0 },
237
- { direction: PortDirection.Bottom, dx: 0, dy: node.size.height / 2 },
238
- { direction: PortDirection.Left, dx: -node.size.width / 2, dy: 0 }
239
- ]
240
- .map((entry) => ({
241
- ...entry,
242
- port: byDirection.get(entry.direction)
243
- }))
244
- .filter((entry): entry is typeof entry & { port: Port } => Boolean(entry.port))
245
- }
246
-
247
- const toCanvasPoint = (event: MouseEvent): Point => {
248
- const svg = svgRef.value
249
- if (!svg) {
250
- return { x: event.clientX, y: event.clientY }
251
- }
252
- const rect = svg.getBoundingClientRect()
253
- return {
254
- x: event.clientX - rect.left,
255
- y: event.clientY - rect.top
256
- }
257
- }
258
-
259
- const toViewportPoint = (point: Point): Point => ({
260
- x: (point.x - props.panX) / zoomLevel.value,
261
- y: (point.y - props.panY) / zoomLevel.value
262
- })
263
-
264
- const applyDragFrame = () => {
265
- frameState.scheduled = false
266
-
267
- if (!dragState.active) return
268
- const currentNode = props.nodes.find((node) => node.id === dragState.nodeId)
269
- if (!currentNode) return
270
-
271
- const position = resolveNodeDragPosition({
272
- node: currentNode,
273
- nodes: props.nodes,
274
- alignThreshold: props.alignThreshold,
275
- clientX: dragState.latestClientX,
276
- clientY: dragState.latestClientY,
277
- offsetX: dragState.offsetX,
278
- offsetY: dragState.offsetY,
279
- canvasWidth: canvasWidth.value / zoomLevel.value,
280
- canvasHeight: canvasHeight.value / zoomLevel.value,
281
- gridSize: props.gridSize,
282
- snapToGrid: props.snapToGrid
283
- })
284
-
285
- emit('nodePositionChange', { nodeId: dragState.nodeId, position })
286
- }
287
-
288
- const resetLinkState = () => {
289
- linkState.active = false
290
- linkState.pendingClick = false
291
- linkState.sourceNodeId = ''
292
- linkState.sourcePortId = ''
293
- }
294
-
295
- const stopDrag = () => {
296
- dragState.active = false
297
- dragState.nodeId = ''
298
-
299
- if (frameState.rafId) {
300
- window.cancelAnimationFrame(frameState.rafId)
301
- frameState.rafId = 0
302
- }
303
- frameState.scheduled = false
304
- }
305
-
306
- const stopLink = () => {
307
- if (!linkState.active && !linkState.pendingClick) return
308
-
309
- const source = resolveLinkSource({
310
- sourceNodeId: linkState.sourceNodeId,
311
- sourcePortId: linkState.sourcePortId,
312
- nodes: props.nodes
313
- })
314
-
315
- if (source) {
316
- const completion = resolveLinkCompletion({
317
- sourceNodeId: source.sourceNode.id,
318
- sourcePortId: source.sourcePort.id,
319
- sourceNode: source.sourceNode,
320
- sourcePort: source.sourcePort,
321
- nodes: props.nodes,
322
- mode: linkState.pendingClick ? 'click' : 'drag',
323
- currentPoint: { x: linkState.currentX, y: linkState.currentY },
324
- hitRadius: props.portHitRadius
325
- })
326
-
327
- if (completion.targetNodeId && completion.targetPortId) {
328
- emit('edgeCreate', {
329
- sourceNodeId: completion.sourceNodeId,
330
- sourcePortId: completion.sourcePortId,
331
- targetNodeId: completion.targetNodeId,
332
- targetPortId: completion.targetPortId
333
- })
334
- }
335
- }
336
-
337
- resetLinkState()
338
- }
339
-
340
- const previewPoints = computed(() => {
341
- if (!linkState.active) return []
342
-
343
- const source = resolveLinkSource({
344
- sourceNodeId: linkState.sourceNodeId,
345
- sourcePortId: linkState.sourcePortId,
346
- nodes: props.nodes
347
- })
348
- if (!source) return []
349
-
350
- return buildPreviewRoute({
351
- sourceNode: source.sourceNode,
352
- sourcePort: source.sourcePort,
353
- nodes: props.nodes,
354
- currentPoint: { x: linkState.currentX, y: linkState.currentY },
355
- hitRadius: props.portHitRadius
356
- }).points
357
- })
358
-
359
- const renderedPreview = computed(() => renderSvgEdge(previewPoints.value, { pixelSnap: true }))
360
-
361
- const onNodeMousedown = (node: GraphNode<unknown>, event: MouseEvent) => {
362
- if (linkState.active) return
363
-
364
- event.preventDefault()
365
- emit('selectionChange', { nodeId: node.id })
366
- dragState.active = true
367
- dragState.nodeId = node.id
368
-
369
- const viewportPoint = toViewportPoint(toCanvasPoint(event))
370
- dragState.offsetX = viewportPoint.x - node.position.x
371
- dragState.offsetY = viewportPoint.y - node.position.y
372
- dragState.latestClientX = viewportPoint.x
373
- dragState.latestClientY = viewportPoint.y
374
- }
375
-
376
- const onPortMousedown = (node: GraphNode<unknown>, port: Port, event: MouseEvent) => {
377
- event.preventDefault()
378
- emit('selectionChange', { nodeId: node.id })
379
-
380
- const start = getPortPoint(node, port)
381
- linkState.active = false
382
- linkState.pendingClick = true
383
- linkState.pressedClientX = event.clientX
384
- linkState.pressedClientY = event.clientY
385
- linkState.sourceNodeId = node.id
386
- linkState.sourcePortId = port.id
387
- linkState.currentX = start.x
388
- linkState.currentY = start.y
389
- }
390
-
391
- const onEdgeMousedown = (edge: GraphEdge<unknown>, event: MouseEvent) => {
392
- event.preventDefault()
393
- event.stopPropagation()
394
- emit('selectionChange', { edgeId: edge.id })
395
- }
396
-
397
- const onCanvasMousedown = () => {
398
- if (linkState.active || dragState.active) return
399
- emit('selectionChange', {})
400
- }
401
-
402
- const onWindowMouseMove = (event: MouseEvent) => {
403
- const canvasPoint = toCanvasPoint(event)
404
-
405
- if (linkState.pendingClick && !linkState.active) {
406
- const shouldActivate = shouldActivateLinkDrag({
407
- pressedClientX: linkState.pressedClientX,
408
- pressedClientY: linkState.pressedClientY,
409
- currentClientX: event.clientX,
410
- currentClientY: event.clientY,
411
- threshold: props.portDragThreshold
412
- })
413
-
414
- if (shouldActivate) {
415
- linkState.active = true
416
- linkState.pendingClick = false
417
- }
418
- }
419
-
420
- if (linkState.active) {
421
- const viewportPoint = toViewportPoint(canvasPoint)
422
- linkState.currentX = viewportPoint.x
423
- linkState.currentY = viewportPoint.y
424
- return
425
- }
426
-
427
- if (!dragState.active) return
428
- const viewportPoint = toViewportPoint(canvasPoint)
429
- dragState.latestClientX = viewportPoint.x
430
- dragState.latestClientY = viewportPoint.y
431
-
432
- if (frameState.scheduled) return
433
- frameState.scheduled = true
434
- frameState.rafId = window.requestAnimationFrame(applyDragFrame)
435
- }
436
-
437
- const onWindowMouseUp = () => {
438
- stopDrag()
439
- stopLink()
440
- }
441
-
442
- const onWindowKeyDown = (event: KeyboardEvent) => {
443
- if (event.key !== 'Delete' && event.key !== 'Backspace') return
444
-
445
- const target = event.target as HTMLElement | null
446
- if (target) {
447
- const tagName = target.tagName
448
- if (tagName === 'INPUT' || tagName === 'TEXTAREA' || target.isContentEditable) {
449
- return
450
- }
451
- }
452
-
453
- if (props.selectedEdgeId) {
454
- event.preventDefault()
455
- emit('edgeDelete', { edgeId: props.selectedEdgeId })
456
- return
457
- }
458
-
459
- if (props.selectedNodeId) {
460
- event.preventDefault()
461
- emit('nodeDelete', { nodeId: props.selectedNodeId })
462
- }
463
- }
464
-
465
- onMounted(() => {
466
- const updateSize = () => {
467
- const host = containerRef.value
468
- if (!host) return
469
- const rect = host.getBoundingClientRect()
470
- if (rect.width > 0) sizeState.width = rect.width
471
- if (rect.height > 0) sizeState.height = rect.height
472
- }
473
-
474
- updateSize()
475
- resizeObserver = new ResizeObserver(updateSize)
476
- if (containerRef.value) resizeObserver.observe(containerRef.value)
477
-
478
- window.addEventListener('mousemove', onWindowMouseMove)
479
- window.addEventListener('mouseup', onWindowMouseUp)
480
- window.addEventListener('keydown', onWindowKeyDown)
481
- })
482
-
483
- onUnmounted(() => {
484
- stopDrag()
485
- resizeObserver?.disconnect()
486
- window.removeEventListener('mousemove', onWindowMouseMove)
487
- window.removeEventListener('mouseup', onWindowMouseUp)
488
- window.removeEventListener('keydown', onWindowKeyDown)
489
- })
490
- </script>
491
-
492
- <template>
493
- <section ref="containerRef" class="canvas-view" :style="{ width: '100%', height: '100%' }">
494
- <div class="zoom-controls">
495
- <button type="button" class="zoom-btn" @click="zoomOut">-</button>
496
- <span class="zoom-text">{{ Math.round(zoomLevel * 100) }}%</span>
497
- <button type="button" class="zoom-btn" @click="zoomIn">+</button>
498
- </div>
499
- <svg
500
- ref="svgRef"
501
- class="canvas-svg"
502
- xmlns="http://www.w3.org/2000/svg"
503
- :viewBox="`0 0 ${canvasWidth} ${canvasHeight}`"
504
- @mousedown.self="onCanvasMousedown"
505
- >
506
- <g :transform="viewportTransform">
507
- <g class="edge-layer">
508
- <template v-for="item in renderedEdges" :key="item.edgeId">
509
- <path
510
- :d="item.path"
511
- fill="none"
512
- stroke="transparent"
513
- stroke-width="15"
514
- vector-effect="non-scaling-stroke"
515
- class="edge-hit"
516
- @mousedown.stop="(event) => onEdgeMousedown(item.edge, event)"
517
- />
518
- <path
519
- :d="item.path"
520
- :stroke="selectedEdgeId === item.edgeId ? '#2563eb' : item.lineColor"
521
- stroke-width="1"
522
- fill="none"
523
- stroke-linecap="round"
524
- stroke-linejoin="round"
525
- vector-effect="non-scaling-stroke"
526
- shape-rendering="geometricPrecision"
527
- class="edge-path"
528
- :class="{ 'edge-flowing': item.flowing }"
529
- :stroke-dasharray="item.lineDasharray ?? (item.flowing ? '7 6' : undefined)"
530
- @mousedown.stop="(event) => onEdgeMousedown(item.edge, event)"
531
- />
532
- <polygon
533
- :points="item.arrow"
534
- :fill="selectedEdgeId === item.edgeId ? '#2563eb' : item.lineColor"
535
- :stroke="selectedEdgeId === item.edgeId ? '#2563eb' : item.lineColor"
536
- vector-effect="non-scaling-stroke"
537
- shape-rendering="geometricPrecision"
538
- class="edge-arrow"
539
- @mousedown.stop="(event) => onEdgeMousedown(item.edge, event)"
540
- />
541
- <text
542
- v-if="item.label && item.labelPoint"
543
- :x="item.labelPoint.x"
544
- :y="item.labelPoint.y"
545
- class="edge-label"
546
- :fill="item.labelColor"
547
- text-anchor="middle"
548
- dominant-baseline="central"
549
- >
550
- {{ item.label }}
551
- </text>
552
- </template>
553
-
554
- <template v-if="previewPoints.length > 1">
555
- <path
556
- :d="renderedPreview.path"
557
- stroke="#94a3b8"
558
- stroke-width="2"
559
- fill="none"
560
- stroke-linecap="round"
561
- stroke-linejoin="round"
562
- vector-effect="non-scaling-stroke"
563
- shape-rendering="geometricPrecision"
564
- />
565
- <polygon
566
- :points="renderedPreview.arrow"
567
- fill="#94a3b8"
568
- stroke="#94a3b8"
569
- vector-effect="non-scaling-stroke"
570
- shape-rendering="geometricPrecision"
571
- />
572
- </template>
573
- </g>
574
-
575
- <g class="node-layer">
576
- <g
577
- v-for="node in nodes"
578
- :key="node.id"
579
- class="node-group"
580
- :transform="`translate(${node.position.x + node.size.width / 2} ${node.position.y + node.size.height / 2})`"
581
- >
582
- <rect
583
- v-if="resolveNodeShape(node) === 'rect'"
584
- :x="-node.size.width / 2"
585
- :y="-node.size.height / 2"
586
- :width="node.size.width"
587
- :height="node.size.height"
588
- rx="4"
589
- ry="4"
590
- :fill="resolveNodeFillColor(node)"
591
- stroke="#0f172a"
592
- :class="{ selected: selectedNodeId === node.id }"
593
- @mousedown.stop="(event) => onNodeMousedown(node, event)"
594
- />
595
-
596
- <rect
597
- v-else-if="resolveNodeShape(node) === 'ellipse'"
598
- :x="-node.size.width / 2"
599
- :y="-node.size.height / 2"
600
- :width="node.size.width"
601
- :height="node.size.height"
602
- :rx="node.size.height / 2"
603
- :ry="node.size.height / 2"
604
- :fill="resolveNodeFillColor(node)"
605
- stroke="#0f172a"
606
- :class="{ selected: selectedNodeId === node.id }"
607
- @mousedown.stop="(event) => onNodeMousedown(node, event)"
608
- />
609
-
610
- <polygon
611
- v-else
612
- :points="diamondPointsOf(node)"
613
- :fill="resolveNodeFillColor(node)"
614
- stroke="#0f172a"
615
- :class="{ selected: selectedNodeId === node.id }"
616
- @mousedown.stop="(event) => onNodeMousedown(node, event)"
617
- />
618
-
619
- <foreignObject
620
- :x="-node.size.width / 2"
621
- :y="-node.size.height / 2"
622
- :width="node.size.width"
623
- :height="node.size.height"
624
- class="node-title-foreign"
625
- >
626
- <div xmlns="http://www.w3.org/1999/xhtml" class="node-title-dom" :style="{ color: resolveNodeFontColor(node) }">
627
- <svg v-if="iconSymbolHrefOf(node)" width="14" height="14" class="node-icon-dom" aria-hidden="true">
628
- <use :href="iconSymbolHrefOf(node)" :fill="resolveNodeFontColor(node)" />
629
- </svg>
630
- <span v-else-if="iconOf(node)" class="node-icon-fallback">{{ iconOf(node) }}</span>
631
- <span class="node-title-text">{{ titleOf(node) }}</span>
632
- </div>
633
- </foreignObject>
634
-
635
- <circle
636
- v-for="item in portsOf(node)"
637
- v-show="selectedNodeId === node.id"
638
- :key="item.port.id"
639
- :cx="item.dx"
640
- :cy="item.dy"
641
- r="5"
642
- fill="#ffffff"
643
- stroke="#1d4ed8"
644
- class="port-dot"
645
- @mousedown.stop="(event) => onPortMousedown(node, item.port, event)"
646
- />
647
- </g>
648
- </g>
649
- </g>
650
- </svg>
651
- </section>
652
- </template>
653
-
654
- <style scoped>
655
- .canvas-view {
656
- position: relative;
657
- overflow: hidden;
658
- border: 1px solid #dee2e6;
659
- background: #f8f9fa;
660
- }
661
-
662
- .zoom-controls {
663
- position: absolute;
664
- top: 8px;
665
- right: 8px;
666
- z-index: 2;
667
- display: inline-flex;
668
- align-items: center;
669
- gap: 4px;
670
- background: rgba(255, 255, 255, 0.95);
671
- border: 1px solid #cbd5e1;
672
- border-radius: 6px;
673
- padding: 4px 6px;
674
- }
675
-
676
- .zoom-btn {
677
- width: 24px;
678
- height: 24px;
679
- border: 1px solid #cbd5e1;
680
- border-radius: 4px;
681
- background: #f8fafc;
682
- cursor: pointer;
683
- line-height: 1;
684
- }
685
-
686
- .zoom-text {
687
- min-width: 44px;
688
- text-align: center;
689
- font-size: 12px;
690
- color: #334155;
691
- }
692
-
693
- .canvas-svg {
694
- width: 100%;
695
- height: 100%;
696
- }
697
-
698
- rect.selected,
699
- polygon.selected {
700
- stroke: #2563eb;
701
- stroke-width: 2;
702
- }
703
-
704
- .port-dot {
705
- cursor: crosshair;
706
- }
707
-
708
- .edge-path,
709
- .edge-arrow {
710
- cursor: pointer;
711
- }
712
-
713
- .edge-flowing {
714
- animation: edge-flow 0.8s linear infinite;
715
- }
716
-
717
- @keyframes edge-flow {
718
- from {
719
- stroke-dashoffset: 0;
720
- }
721
- to {
722
- stroke-dashoffset: -13;
723
- }
724
- }
725
-
726
- .edge-label {
727
- font-size: 12px;
728
- pointer-events: none;
729
- user-select: none;
730
- }
731
-
732
- .node-title-foreign {
733
- pointer-events: none;
734
- }
735
-
736
- .node-title-dom {
737
- width: 100%;
738
- height: 100%;
739
- display: flex;
740
- align-items: center;
741
- justify-content: center;
742
- gap: 6px;
743
- font-size: 13px;
744
- user-select: none;
745
- white-space: nowrap;
746
- overflow: hidden;
747
- text-overflow: ellipsis;
748
- }
749
-
750
- .node-icon-dom,
751
- .node-icon-fallback {
752
- flex: 0 0 auto;
753
- }
754
-
755
- .node-title-text {
756
- overflow: hidden;
757
- text-overflow: ellipsis;
758
- }
759
- </style>
package/src/EdgeView.vue DELETED
@@ -1,46 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed } from 'vue'
3
- import type { Point } from '@workflow/core'
4
- import { renderSvgEdge } from '@workflow/svg-renderer'
5
-
6
- const props = withDefaults(
7
- defineProps<{
8
- points: Point[]
9
- stroke?: string
10
- strokeWidth?: number
11
- fill?: string
12
- }>(),
13
- {
14
- stroke: '#0b7285',
15
- strokeWidth: 1,
16
- fill: '#0b7285'
17
- }
18
- )
19
-
20
- const render = computed(() => renderSvgEdge(props.points))
21
- </script>
22
-
23
- <template>
24
- <svg class="edge-view" xmlns="http://www.w3.org/2000/svg">
25
- <path
26
- :d="render.path"
27
- :stroke="stroke"
28
- :stroke-width="strokeWidth"
29
- fill="none"
30
- stroke-linecap="round"
31
- stroke-linejoin="round"
32
- />
33
- <polygon :points="render.arrow" :fill="fill" :stroke="stroke" />
34
- </svg>
35
- </template>
36
-
37
- <style scoped>
38
- .edge-view {
39
- position: absolute;
40
- inset: 0;
41
- width: 100%;
42
- height: 100%;
43
- overflow: visible;
44
- pointer-events: none;
45
- }
46
- </style>
package/src/index.ts DELETED
@@ -1 +0,0 @@
1
- export { default as CanvasView } from './CanvasView.vue'