diff --git a/README.md b/README.md
index 0c13837..84dc851 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,7 @@ https://pflow-app.fly.dev/
* export to gno.land
* export to solidity
* export to js/ts
+ * export to python
* support analysis
* export to julia (jupyter notebook)
@@ -30,3 +31,11 @@ https://pflow-app.fly.dev/
See [widget.html](./widget.html) for an example of how to next the pflow viewer in an html page.
[embed-example](https://pflow.dev/embed/?m=petriNet&v=v0&p=place0&i=1&c=3&o=0&x=130&y=207&p=place1&i=0&c=0&o=1&x=395&y=299&t=txn0&x=46&y=116&t=txn1&x=227&y=112&t=txn2&x=43&y=307&t=txn3&x=235&y=306&s=txn0&e=place0&w=1&s=place0&e=txn1&w=3&s=txn2&e=place0&n=1&w=3&s=place0&e=txn3&n=1&w=1&s=txn3&e=place1&w=1)
+
+## Test model with shortURL v1
+
+[test-model](http://localhost:3000/?m=PetriNet&v=v1&p=place0&c=3&i=1&o=0&x=130&y=207&t=txn0&x=46&y=116&t=txn1&x=227&y=112&t=txn2&x=43&y=307&t=txn3&x=235&y=306&s=txn0&e=place0&w=1&s=place0&e=txn1&w=3&s=txn2&e=place0&n=1&w=3&s=place0&e=txn3&n=1&w=1)
+
+```
+http://localhost:3000/?m=PetriNet&v=v1&p=place0&c=3&i=1&o=0&x=130&y=207&t=txn0&x=46&y=116&t=txn1&x=227&y=112&t=txn2&x=43&y=307&t=txn3&x=235&y=306&s=txn0&e=place0&w=1&s=place0&e=txn1&w=3&s=txn2&e=place0&n=1&w=3&s=place0&e=txn3&n=1&w=1
+```
diff --git a/TODO.md b/TODO.md
index 7c473b4..3cba0bc 100644
--- a/TODO.md
+++ b/TODO.md
@@ -6,16 +6,27 @@ build of app.pflow.dev - no wallet connector - minimal build/viewer
WIP
---
-- [ ] Review roadmap.md - consider deploying gnoland-only version first
- [ ] gno.land version needs multi-token support
- [ ] url-visualizer on gno.land to build out multi-step actions
-- [ ] in this project use ./static/form.html and ./static/model.svg as template
+- [ ] fix token colors - changing colors in the editor should change the diagram
+- [ ] fix capacity set to 0 does not remove capacity limitation
+- [ ] test that permalink gets updated on edit
+- [ ] minURL - add support for multi-token colors
+
+
+ DONE
+----
+- [x] fix editor interactions - issue selecting text and last few lines of code
+- [x] in this project use ./static/form.html and ./static/model.svg as template
+- [x] Review roadmap.md - consider deploying gnoland-only version first
BACKLOG
-------
- [ ] fix failing tests
- [ ] complete upgrades for colored tokens
- [ ] check backward-compatible support for URL formats
+- [ ] consider adopting https://github.com/microsoft/monaco-editor/tree/main for multi-language support
+- [ ] review the plan to implement custom lexer/parser for go, julia, solidity, python
```
/?foo=1&bar=1&baz=1
````
@@ -40,5 +51,3 @@ ICEBOX
- [ ] exploring dom updates:update object.data
- [ ] vs live updates inside an embedded SVG using postMessage().
-DONE
-----
diff --git a/public/model.svg b/public/model.svg
index 08f7e5e..27ce4d9 100644
--- a/public/model.svg
+++ b/public/model.svg
@@ -1,310 +1,401 @@
-
diff --git a/src/App.css b/src/App.css
index c1ad4dc..1fcc316 100644
--- a/src/App.css
+++ b/src/App.css
@@ -25,6 +25,7 @@ body {
outline: none;
margin-left: 15px;
margin-top: 40px;
+ overflow: auto;
}
#controls {
diff --git a/src/App.tsx b/src/App.tsx
index ae49711..b927bba 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react';
import { Model, importUrl, ModelData } from './model/model';
import './App.css';
import CodeEditor from "@uiw/react-textarea-code-editor";
- import rehypePrism from "rehype-prism-plus";
const defaultModel: ModelData = {
"modelType": "PetriNet",
@@ -64,7 +63,7 @@ import React, { useEffect, useState } from 'react';
sourceElement.style.height = `${Math.max(height, 300)}px`;
sourceElement.style.width = `${Math.max(width - 40, 300)}px`;
}
- setEditorHeight(window.innerHeight - 680);
+ setEditorHeight(height);
};
handleResize();
@@ -90,7 +89,12 @@ import React, { useEffect, useState } from 'react';
-
+
+
+
+
+
+
@@ -125,18 +129,14 @@ import React, { useEffect, useState } from 'react';
value={modelState.toJson()}
data-color-mode="light"
language={"js"}
- placeholder="source"
- rehypePlugins={[
- [rehypePrism, { ignoreMissing: true, showLineNumbers: false }],
- ]}
+ placeholder="source v1"
padding={10}
// @ts-ignore
style={{
- fontSize: 14,
minHeight: editorHeight,
backgroundColor: "#FFFFFF",
border: "1px solid #E0E0E0",
- fontFamily: "ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace",
+ fontFamily: "monospace",
}}
onChange={(evt) => {
try {
diff --git a/static/editor.html b/static/editor.html
deleted file mode 100644
index b02be85..0000000
--- a/static/editor.html
+++ /dev/null
@@ -1,263 +0,0 @@
-
-
-
-
-
- pflow | model.svg
-
-
-
-
-
-
-
- pflow.xyz - PetriNet Metamodels
-
-
-
-
-
-
-
-
-
-
-
-
- Status: Ready
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/static/index.html b/static/index.html
index a0349d0..3281a18 100644
--- a/static/index.html
+++ b/static/index.html
@@ -1,20 +1,273 @@
-
+
+
+
- pflow | metamodel
+ pflow | model.svg
-
+
+
+
+
+ pflow.xyz - PetriNet Metamodels
+
+
+
+
+
+
+
+
+
+
+
+
+ Status: Ready
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/static/model.svg b/static/model.svg
index 619d35f..f41fe41 100644
--- a/static/model.svg
+++ b/static/model.svg
@@ -1,23 +1,13 @@
-
-
-
+
+
-
-
+
+
@@ -28,272 +18,5 @@
"arcs": []
}
-
-
\ No newline at end of file
+
+
diff --git a/static/pflow.css b/static/pflow.css
new file mode 100644
index 0000000..b496b54
--- /dev/null
+++ b/static/pflow.css
@@ -0,0 +1,11 @@
+.place { fill: #ffffff; stroke: #000000; stroke-width: 1.5; }
+.transition { fill: #ffffff; stroke: #000000; stroke-width: 1.5; cursor: pointer; user-select: text;}
+.transition.enabled { fill: #62fa75 }
+.transition.inhibited { fill: #fab5b0; stroke: #000000; stroke-width: 1.5; cursor: pointer; user-select: text;}
+.arc { stroke: #000000; stroke-width: 1; }
+.label { font-size: small; font-family: sans-serif; fill: #000000; user-select: none; }
+.token { fill: #000000; stroke: gray; stroke-width: 0.5; }
+.tokenSmall { font-size: small; user-select: none; font-weight: bold; }
+.red { fill: #ff0000; color: #ff0000; }
+.green { fill: #00ff00; color: #00ff00; }
+.blue { fill: #0000ff; color: #0000ff; }
diff --git a/static/pflow.js b/static/pflow.js
new file mode 100644
index 0000000..ea42b65
--- /dev/null
+++ b/static/pflow.js
@@ -0,0 +1,363 @@
+let petriNet = {};
+let sequence = 0;
+
+function arcEndpoints(x1, y1, x2, y2) {
+ const length = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
+ const shorten = 22;
+ const ratio = shorten / length;
+
+ const newX1 = x1 + (x2 - x1) * ratio;
+ const newY1 = y1 + (y2 - y1) * ratio;
+ const newX2 = x2 - (x2 - x1) * ratio;
+ const newY2 = y2 - (y2 - y1) * ratio;
+
+ const midX = (newX1 + newX2) / 2;
+ const midY = (newY1 + newY2) / 2;
+
+ return {
+ x1: newX1, y1: newY1, x2: newX2, y2: newY2, midX, midY
+ };
+}
+
+function createElements() {
+ const svg = document.querySelector("svg");
+ const fragment = document.createDocumentFragment();
+
+ // Create arcs
+ petriNet.arcs.forEach(arc => {
+ // REVIEW: places and transitions must not re-use IDs, ensure unique IDs for arcs
+ const source = petriNet.places[arc.source] || petriNet.transitions[arc.source];
+ const target = petriNet.places[arc.target] || petriNet.transitions[arc.target];
+ if (!source || !target) {
+ console.error(`Source or target not found for arc: ${arc}`);
+ return;
+ }
+ const { x1, y1, x2, y2, midX, midY } = arcEndpoints(source.x, source.y, target.x, target.y);
+
+ let path = document.createElementNS("http://www.w3.org/2000/svg", "line");
+ path.setAttribute("class", "arc");
+ path.setAttribute("x1", x1);
+ path.setAttribute("y1", y1);
+ path.setAttribute("x2", x2);
+ path.setAttribute("y2", y2);
+ path.setAttribute("class", "arc");
+ if (arc.inhibit && arc.inhibit === true) {
+ path.setAttribute("marker-end", "url(#markerInhibit1)");
+ } else {
+ path.setAttribute("marker-end", "url(#markerArrow1)");
+ }
+ fragment.appendChild(path);
+
+ const angle = Math.atan2(y2 - y1, x2 - x1);
+ let x
+ let y
+
+ if (x2 < x1) {
+ x = midX + 7 * Math.cos(angle + Math.PI / 2);
+ y = midY + 7 * Math.sin(angle + Math.PI / 2);
+ } else {
+ x = midX - 7 * Math.cos(angle + Math.PI / 2);
+ y = midY - 7 * Math.sin(angle + Math.PI / 2);
+ }
+
+ let color = "black"; // colorize token weights
+ for (let i = 0; i < arc.weight.length; i++) {
+ if (arc.weight[i] > 0) {
+ color = petriNet.tokens[i];
+ break;
+ }
+ }
+
+ let text = document.createElementNS("http://www.w3.org/2000/svg", "text");
+ text.setAttribute("x", x);
+ text.setAttribute("y", y);
+ text.setAttribute("class", "label "+color);
+ text.textContent = arc.weight[0];
+ fragment.appendChild(text);
+ });
+
+ // Create places
+ Object.entries(petriNet.places).forEach(([id, place]) => {
+ let circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
+ circle.setAttribute("id", id);
+ circle.setAttribute("class", "place");
+ circle.setAttribute("cx", place.x);
+ circle.setAttribute("cy", place.y);
+ circle.setAttribute("r", "16");
+ fragment.appendChild(circle);
+
+ let text = document.createElementNS("http://www.w3.org/2000/svg", "text");
+ text.setAttribute("x", place.x - 18);
+ text.setAttribute("y", place.y - 20);
+ text.setAttribute("class", "label");
+ text.textContent = id;
+ fragment.appendChild(text);
+ });
+
+ // Create transitions
+ Object.entries(petriNet.transitions).forEach(([id, transition]) => {
+ let rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ rect.setAttribute("id", id);
+ let enabled = canFire(id);
+ let inhibited = isInhibited(id);
+ console.log({id, enabled, inhibited })
+ if (inhibited) {
+ rect.setAttribute("class", "transition inhibited");
+ } else if (enabled) {
+ rect.setAttribute("class", "transition enabled");
+ } else {
+ rect.setAttribute("class", "transition");
+ }
+ rect.setAttribute("x", transition.x - 15);
+ rect.setAttribute("y", transition.y - 15);
+ rect.setAttribute("rx", "5");
+ rect.setAttribute("width", "30");
+ rect.setAttribute("height", "30");
+ fragment.appendChild(rect);
+
+ let text = document.createElementNS("http://www.w3.org/2000/svg", "text");
+ text.setAttribute("x", transition.x - 15);
+ text.setAttribute("y", transition.y - 20);
+
+ text.setAttribute("class", "label");
+ text.textContent = id;
+ fragment.appendChild(text);
+
+ rect.addEventListener('click', () => fireTransition(id));
+ });
+
+ document.querySelectorAll(".place, .transition, .arc, .label").forEach(e => e.remove());
+ svg.appendChild(fragment);
+}
+
+function canFire(id) {
+ let sourceArcs = petriNet.arcs.filter(arc => arc.target === id && !arc.inhibit);
+ let targetArcs = petriNet.arcs.filter(arc => arc.source === id && !arc.inhibit);
+ let enabled = true;
+ sourceArcs.forEach(arc => {
+ let place = petriNet.places[arc.source];
+ for (let i = 0; i < place.tokens.length; i++) {
+ if (arc.weight[i] > place.tokens[i]) {
+ enabled = false;
+ return;
+ }
+ }
+ });
+ targetArcs.forEach(arc => {
+ let place = petriNet.places[arc.target];
+ for (let i = 0; i < place.tokens.length; i++) {
+ console.log({arc, place, out: arc.weight[i] + place.tokens[i], cap: place.capacity[i]
+ })
+ if (arc.weight[i] + place.tokens[i] > place.capacity[i]) {
+ enabled = false;
+ return;
+ }
+ }
+ });
+ return enabled;
+}
+
+function isInhibited(id) {
+ let sourceArcs = petriNet.arcs.filter(arc => (arc.target === id && arc.inhibit === true));
+ let targetArcs = petriNet.arcs.filter(arc => (arc.source === id && arc.inhibit === true));
+ let inhibited = false;
+ sourceArcs.forEach(arc => {
+ let place = petriNet.places[arc.source];
+ for (let i = 0; i <= place.tokens.length; i++) {
+ if (place.tokens[i] >= arc.weight[i]) {
+ inhibited = true;
+ return
+ }
+ }
+ });
+ targetArcs.forEach(arc => {
+ let place = petriNet.places[arc.target];
+ for (let i = 0; i < place.tokens.length; i++) {
+ if (place.tokens[i] < arc.weight[i]) {
+ inhibited = true;
+ }
+ }
+ });
+ return inhibited;
+}
+
+function fireTransition(id, dryRun = false) {
+ const exists = petriNet.transitions && petriNet.transitions[id]
+ const inhibited = isInhibited(id)
+ const enabled = canFire(id)
+ if (!exists) {
+ console.error(`Transition '${id}' not found in petriNet.transitions`);
+ return;
+ } else if (inhibited) {
+ console.warn(`Transition '${id}' is inhibited`);
+ return;
+ } else if (!enabled) {
+ console.warn(`Transition '${id}' is not enabled`);
+ return;
+ } else if (!dryRun) {
+ let sourceArcs = petriNet.arcs.filter(arc => arc.target === id && !arc.inhibit);
+ let targetArcs = petriNet.arcs.filter(arc => arc.source === id && !arc.inhibit);
+ sourceArcs.forEach(arc => {
+ let place = petriNet.places[arc.source];
+ for (let i = 0; i < place.tokens.length; i++) {
+ place.tokens[i] -= arc.weight[i];
+ }
+ });
+ targetArcs.forEach(arc => {
+ let place = petriNet.places[arc.target];
+ for (let i = 0; i < place.tokens.length; i++) {
+ place.tokens[i] += arc.weight[i];
+ }
+ });
+ updateTokens();
+ window.parent.postMessage({
+ type: 'transitionFired',
+ sequence: ++sequence,
+ transitionId: id,
+ petriNet: petriNet
+ }, '*');
+ }
+ return true;
+}
+
+function updateTokens() {
+ document.querySelectorAll(".token, .tokenSmall").forEach(e =>e.remove());
+ const fragment = document.createDocumentFragment();
+ Object.entries(petriNet.places).forEach(([, place]) => {
+ // Create tokens
+ for (let i = 0; i < place.tokens.length; i++) {
+ if (place.tokens[i] === 0) continue;
+ if (place.tokens[i] === 1) {
+ let token = document.createElementNS("http://www.w3.org/2000/svg", "circle");
+ token.setAttribute("cx", place.x);
+ token.setAttribute("cy", place.y);
+ token.setAttribute("r", "3");
+ token.setAttribute("class", "token " + petriNet.tokens[i]);
+ fragment.appendChild(token);
+ } else if (place.tokens[i] > 1) {
+ let text = document.createElementNS("http://www.w3.org/2000/svg", "text");
+ text.setAttribute("x", place.x - 3);
+ text.setAttribute("y", place.y + 4);
+ text.setAttribute("class", "tokenSmall " + petriNet.tokens[i]);
+ text.textContent = place.tokens[i];
+ fragment.appendChild(text);
+ }
+ }
+ });
+
+ // update transition colors
+ Object.entries(petriNet.transitions).forEach(([id, transition]) => {
+ let rect = document.getElementById(id);
+ let enabled = canFire(id);
+ let inhibited = isInhibited(id);
+ if (inhibited) {
+ rect.setAttribute("class", "transition inhibited");
+ } else if (enabled) {
+ rect.setAttribute("class", "transition enabled");
+ } else {
+ rect.setAttribute("class", "transition");
+ }
+ })
+ document.querySelector("svg").appendChild(fragment);
+}
+
+function resetPetriNet() {
+ Object.entries(petriNet.places).forEach(([, place]) => {
+ place.tokens = [...place.initial];
+ });
+ createElements();
+ updateTokens();
+ sequence = 0;
+ window.parent.postMessage({
+ type: 'reset',
+ sequence: sequence,
+ petriNet: petriNet
+ }, '*');
+}
+
+function init() {
+ let metadataElement = document.getElementById("metadata");
+ if (metadataElement) {
+ let metadata = metadataElement.textContent.trim();
+ try {
+ petriNet = JSON.parse(metadata);
+ createElements();
+ updateTokens();
+ } catch (error) {
+ console.error("Failed to parse metadata: ", error);
+ }
+ } else {
+ console.error("Metadata element not found");
+ }
+
+ window.addEventListener('message', (event) => {
+ console.log("Received message from parent window: ", event.data);
+ if (event.data.type === 'resize') {
+ resizeSvg(event.data.width, event.data.height);
+ }
+ if (event.data.type === 'setModel') {
+ try {
+ // Expecting a JSON string for the petri net
+ //petriNet = JSON.parse(event.data.model);
+ petriNet = event.data.model;
+ // check for tokens
+ if (!petriNet.tokens) {
+ petriNet.tokens = ["black"]; // fallback to default
+ }
+ // populate place.tokens if not already set
+ Object.entries(petriNet.places).forEach(([, place]) => {
+ if (!place.tokens || place.tokens.length === 0) {
+ place.tokens = [...place.initial]; // fallback to initial if tokens not set
+ }
+ });
+ createElements();
+ updateTokens();
+ } catch (error) {
+ console.error("Failed to set model: ", error);
+ }
+ }
+ if (event.data.type === 'restart') {
+ resetPetriNet();
+ }
+ });
+}
+
+function resizeSvg(width, height) {
+ const svg = document.querySelector("svg");
+ if (svg) {
+ svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
+ svg.setAttribute("width", width); // set width
+ svg.setAttribute("height", height); // set height
+ } else {
+ console.error("SVG element not found for resizing");
+ }
+}
+
+window.addEventListener('keydown', function(event) {
+ if (event.key === 'Escape' || event.key === 'x' || event.key === 'X') {
+ resetPetriNet(); // Reset the Petri net when 'x' is pressed
+ }
+});
+
+window.addEventListener('message', (event) => {
+ if (event.data.type === 'resize') {
+ resizeSvg(event.data.width, event.data.height);
+ }
+ if (event.data.type === 'setModel') {
+ try {
+ // Expecting a JSON string for the petri net
+ //petriNet = JSON.parse(event.data.model);
+ petriNet = event.data.model;
+ createElements();
+ updateTokens();
+ } catch (error) {
+ console.error("Failed to set model: ", error);
+ }
+ }
+ if (event.data.type === 'restart') {
+ resetPetriNet();
+ }
+});
+
+window.addEventListener('load', init);
+document.addEventListener('DOMContentLoaded', init);
\ No newline at end of file