diff --git a/examples/bezier-js/.gitignore b/examples/bezier-js/.gitignore new file mode 100644 index 000000000..69c575d17 --- /dev/null +++ b/examples/bezier-js/.gitignore @@ -0,0 +1,3 @@ +build/ +dist/ +node_modules/ diff --git a/examples/bezier-js/README.md b/examples/bezier-js/README.md new file mode 100644 index 000000000..479063e0d --- /dev/null +++ b/examples/bezier-js/README.md @@ -0,0 +1,22 @@ +# JointJS List Demo + +## Setup + +Use Yarn to run this demo. + +You need to build *JointJS* first. Navigate to the root folder and run: +```bash +yarn install +yarn run build +``` + +Navigate to this directory, then run: +```bash +yarn start +``` + +## License + +The *JointJS* library is licensed under the [Mozilla Public License 2.0](https://github.com/clientIO/joint/blob/master/LICENSE). + +Copyright © 2013-2025 client IO diff --git a/examples/bezier-js/css/bezier.css b/examples/bezier-js/css/bezier.css new file mode 100644 index 000000000..400324d85 --- /dev/null +++ b/examples/bezier-js/css/bezier.css @@ -0,0 +1,8 @@ +#paper-container { + position: absolute; + right: 0; + top: 0; + left: 0; + bottom: 0; + overflow: scroll; + } diff --git a/examples/bezier-js/index.html b/examples/bezier-js/index.html new file mode 100644 index 000000000..af0341cf5 --- /dev/null +++ b/examples/bezier-js/index.html @@ -0,0 +1,13 @@ + + + + + + + Bezier JS Example | JointJS + + +
+ + + diff --git a/examples/bezier-js/package.json b/examples/bezier-js/package.json new file mode 100644 index 000000000..a037a45a8 --- /dev/null +++ b/examples/bezier-js/package.json @@ -0,0 +1,35 @@ +{ + "name": "@joint/demo-bezier-js", + "version": "4.1.3", + "main": "src/index.js", + "homepage": "https://jointjs.com", + "author": { + "name": "client IO", + "url": "https://client.io" + }, + "license": "MPL-2.0", + "private": true, + "installConfig": { + "hoistingLimits": "workspaces" + }, + "scripts": { + "start": "webpack-dev-server", + "tsc": "tsc" + }, + "dependencies": { + "@joint/core": "workspace:^", + "bezier-js": "^6.1.4" + }, + "devDependencies": { + "css-loader": "3.5.3", + "style-loader": "1.2.1", + "webpack": "^5.61.0", + "webpack-cli": "^4.8.0", + "webpack-dev-server": "^4.2.1" + }, + "volta": { + "node": "16.18.1", + "npm": "8.19.2", + "yarn": "3.4.1" + } +} diff --git a/examples/bezier-js/src/index.js b/examples/bezier-js/src/index.js new file mode 100644 index 000000000..6d57c1d0b --- /dev/null +++ b/examples/bezier-js/src/index.js @@ -0,0 +1,190 @@ +import { dia, shapes, util, g, linkTools } from "@joint/core"; +import { Bezier } from "bezier-js"; + +import "../css/bezier.css"; + +class BezierLinkView extends dia.LinkView { + updateDOMSubtreeAttributes() { + const method = this.model.get("outline") ? outlinePath : offsetPath; + + const thickness1 = 20; + const thickness2 = 30; + const thickness3 = 10; + const fillOpacity = 0.3; + const roundDecimals = 0; + const strokeWidth = 2; + const useFill = method !== offsetPath; + + const { line1, line2, line3 } = this.selectors; + + const path1 = this.getConnection(); + path1.round(roundDecimals); + line1.setAttribute("stroke", "red"); + line1.setAttribute("stroke-width", strokeWidth); + line1.setAttribute("fill", useFill ? "red" : "none"); + line1.setAttribute("fill-opacity", fillOpacity); + line1.setAttribute("fill-rule", "nonzero"); + line1.setAttribute("d", method(path1, thickness1)); + + const path2 = new g.Path(offsetPath(path1, thickness1 + thickness2)); + path2.round(roundDecimals); + line2.setAttribute("stroke", "blue"); + line2.setAttribute("stroke-width", strokeWidth); + line2.setAttribute("fill", useFill ? "blue" : "none"); + line2.setAttribute("fill-opacity", fillOpacity); + line2.setAttribute("fill-rule", "nonzero"); + line2.setAttribute("d", method(path2, thickness2)); + + const path3 = new g.Path(offsetPath(path1, -(thickness1 + thickness3))); + path3.round(roundDecimals); + line3.setAttribute("stroke", "green"); + line3.setAttribute("stroke-width", strokeWidth); + line3.setAttribute("fill", useFill ? "green" : "none"); + line3.setAttribute("fill-opacity", fillOpacity); + line3.setAttribute("fill-rule", "nonzero"); + line3.setAttribute("d", method(path3, thickness3)); + } +} + +const graph = new dia.Graph({}, { cellNamespace: shapes }); +const paper = new dia.Paper({ + width: "100%", + height: "100%", + model: graph, + overflow: true, + cellViewNamespace: shapes, + linkView: BezierLinkView, +}); + +document.getElementById("paper-container").appendChild(paper.el); + +const link1 = new dia.Link({ + type: "bezier", + source: { x: 40, y: 100 }, + target: { x: 740, y: 100 }, + vertices: [{ x: 401, y: 208 }], + // vertices: [{ x: 422, y: 417 }], // rounding issues + // vertices: [{ x: 204, y: 63 }], // can not create offset + connector: { name: "smooth" }, + markup: util.svg` + + + + `, +}); + +const link2 = link1.clone().translate(0, 300).set("outline", true); +graph.resetCells([link1, link2]); + +function offsetPath(path, offset) { + const offsetBezierCurves = pathToBezierCurves(path) + .map((bezier) => { + const polyBezier = bezier.offset(offset); + return polyBezier.map((b) => { + if (isNaN(b.points[0].x)) { + console.warn("Unable to create offset", bezier); + return bezier; + } + return b; + }); + }) + .flat(); + let d = ""; + for (let i = 0; i < offsetBezierCurves.length; i++) { + d += offsetBezierCurves[i].toSVG(); + } + return d; +} + +function outlinePath(path, o) { + const outlines = pathToBezierCurves(path).map((curve1) => { + let curves; + try { + curves = curve1.outline(o).curves; + } catch (e) { + console.warn("Caught exception in bezier-js", curve1); + return curve1; + } + return curves.map((curve2) => { + if (isNaN(curve2.points[0].x)) { + console.warn("Unable to create outline", curve1); + return curve1; + } + return curve2; + }); + }); + let d = "M 0 0"; + for (let i = 0; i < outlines.length; i++) { + const outline = outlines[i]; + for (let j = 0; j < outline.length; j++) { + let segmentPath = outline[j].toSVG(); + if (j > 0) { + // Remove the first moveTo command + let index = segmentPath.search(/[C,Q]/); + if (index > 0) { + segmentPath = segmentPath.slice(index); + } + } + d += segmentPath; + } + } + d += "Z"; + return d; +} + +function pathToBezierCurves(path) { + const segments = path.segments; + const bezierCurves = []; + for (let i = 0; i < segments.length; i++) { + const curve = segments[i]; + // Note: JointJS path use only absolute commands + // it's safe to ignore all + if (curve.type === "M") continue; + const { + start, + end, + controlPoint1 = start, + controlPoint2 = end, + } = curve; + const bezier = new Bezier( + start.x, + start.y, + controlPoint1.x, + controlPoint1.y, + controlPoint2.x, + controlPoint2.y, + end.x, + end.y + ); + bezierCurves.push(bezier); + } + return bezierCurves; +} + +// Interactions + +graph.getLinks().forEach((link) => { + const tools = [ + new linkTools.Vertices({ + vertexAdding: true, + redundancyRemoval: false, + }), + new linkTools.SourceArrowhead(), + new linkTools.TargetArrowhead(), + ]; + + link.findView(paper).addTools( + new dia.ToolsView({ + tools: tools, + }) + ); +}); + +// Failing examples from bezier-js + +// const b1 = new Bezier(40, 100, 159, 323, 277, 545, 394, 545); +// console.log(b1.outline(50)); +// console.log(b1.outlineshapes(50)); + +// const b2 = new Bezier(100, 100, 100, 100, 200, 200, 200, 200); +// console.log(b2.outline(50).curves); diff --git a/examples/bezier-js/webpack.config.js b/examples/bezier-js/webpack.config.js new file mode 100644 index 000000000..20663a5d3 --- /dev/null +++ b/examples/bezier-js/webpack.config.js @@ -0,0 +1,29 @@ +const path = require('path'); + +module.exports = { + resolve: { + extensions: ['.js'] + }, + entry: './src/index.js', + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, 'dist'), + publicPath: '/dist/' + }, + mode: 'development', + module: { + rules: [ + { + test: /\.css$/, + sideEffects: true, + use: ['style-loader', 'css-loader'], + } + ] + }, + devServer: { + static: { + directory: __dirname, + }, + compress: true + }, +};