From a66048f0ffc48555b0e7ec13c8bc52d7b1dfe20f Mon Sep 17 00:00:00 2001 From: ys_teng <58208381+YeeShin504@users.noreply.github.com> Date: Sun, 22 Mar 2026 08:52:35 +0800 Subject: [PATCH 1/8] Add variable arrow thickness --- docs/editor.js | 7 ++++--- src/layout.ts | 13 ++++++++++--- src/parser.ts | 12 +++++++----- src/renderer.ts | 12 ++++++------ 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/docs/editor.js b/docs/editor.js index 55674a0..e1e82a6 100644 --- a/docs/editor.js +++ b/docs/editor.js @@ -83,6 +83,7 @@ const REFERENCE_DATA = [ items: [ { title: "Normal Arrow", syntax: 'a -> b : "label"', example: 'a -> b : "Request"' }, { title: "Thick Arrow", syntax: 'a => b : "label"', example: 'a => b : "Big Data"' }, + { title: "Variable Thickness", syntax: 'a =>[thickness] b : "label"', example: 'a =>[0.5] b : "Thin"' }, { title: "Corrupt Arrow", syntax: 'a ~> b : "label"', example: 'a ~> b : "Fragmented"' }, { title: "Dropped Message", syntax: 'a -x b : "label"', example: 'a -x b : "Timeout"' }, { title: "Time Offsets", syntax: 'a @1.5 -> b @1', example: 'a @1.5 -> b @1 : "Relative Time"' } @@ -92,7 +93,7 @@ const REFERENCE_DATA = [ group: "Arrow Styles", items: [ { title: "Arrow Head Size", syntax: 'def arrowHeadSize 10px', example: 'def arrowHeadSize 15px' }, - { title: "Thick Thickness", syntax: 'def thickArrowThickness 40px', example: 'def thickArrowThickness 20px' }, + { title: "Thick Ratio", syntax: 'def thickArrowThickness 1.0', example: 'def thickArrowThickness 0.5' }, { title: "Label Offset", syntax: 'def labelOffset 10px', example: 'def labelOffset 20px' }, { title: "Message Font Size", syntax: 'def messageFontSize 15px', example: 'def messageFontSize 18px' } ] @@ -221,8 +222,8 @@ b ~> a : "corrupted reply" a -x b : "dropped" b -x a : "dropped reply" -a => b : "thick" -b => a : "thick reply" +a => b : "thick reply" +b =>[1.5] a : "thicker reply" a @2 < "left label @2" b @5 > "right label @5"`; diff --git a/src/layout.ts b/src/layout.ts index 5d5aea9..ca49949 100644 --- a/src/layout.ts +++ b/src/layout.ts @@ -15,6 +15,7 @@ export interface ArrowPos { x2: number; y2: number; arrowType: ArrowType; + thickness: number; label?: string; } @@ -95,6 +96,12 @@ export function resolveLayout(entities: Entities): Diagram { endY = action.end; } + const currentThicknessRatio = (action.arrowType === "thick") + ? (action.thicknessRatio ?? settings.thickArrowThickness) + : 0; + + const thickness = currentThicknessRatio * settings.timeTickInterval; + draws.push({ type: action.type, x1: participantsX.get(action.from), @@ -102,12 +109,12 @@ export function resolveLayout(entities: Entities): Diagram { x2: participantsX.get(action.to), y2: height + endY * settings.timeTickInterval, arrowType: action.arrowType, + thickness, label: action.label, }); - const thicknessRatio = action.arrowType === "thick" ? settings.thickArrowThickness / settings.timeTickInterval : 0; - counterMax = Math.max(counterMax, startY + thicknessRatio, endY + thicknessRatio); - counter = endY + thicknessRatio; + counterMax = Math.max(counterMax, startY + currentThicknessRatio, endY + currentThicknessRatio); + counter++; break; case "annotation": diff --git a/src/parser.ts b/src/parser.ts index cb34f01..a686eca 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -17,6 +17,7 @@ export interface Arrow { start?: number; end?: number; arrowType: ArrowType; + thicknessRatio?: number; label?: string; } @@ -65,7 +66,7 @@ export interface Settings { const DEFAULT_SETTINGS: Settings = { arrowHeadSize: 10, dropCrossSize: 12, - thickArrowThickness: 40, + thickArrowThickness: 1.0, labelOffset: 10, corruptStartRatio: 0.85, dropStartRatio: 0.85, @@ -214,7 +215,7 @@ export function parse(src: string): Entities { // arrows const arrowMatch = line.match( - /^(\w+)(?:\s*@([\d.%px]+))?\s*(->|=>|~>|-x)\s*(\w+)(?:\s*@([\d.%px]+))?(?:\s*:\s*"(.+)")?/ + /^(\w+)(?:\s*@([\d.%px]+))?\s*(->|=>|~>|-x)(?:\[([\d.]+)\])?\s*(\w+)(?:\s*@([\d.%px]+))?(?:\s*:\s*"(.+)")?/ ); if (arrowMatch) { @@ -231,9 +232,10 @@ export function parse(src: string): Entities { from: arrowMatch[1], start: arrowMatch[2] ? (parseNumber(arrowMatch[2]) ?? undefined) : undefined, arrowType: typeMap[arrowMatch[3]], - to: arrowMatch[4], - end: arrowMatch[5] ? (parseNumber(arrowMatch[5]) ?? undefined) : undefined, - label: arrowMatch[6] + thicknessRatio: arrowMatch[4] ? parseFloat(arrowMatch[4]) : undefined, + to: arrowMatch[5], + end: arrowMatch[6] ? (parseNumber(arrowMatch[6]) ?? undefined) : undefined, + label: arrowMatch[7] }); continue; diff --git a/src/renderer.ts b/src/renderer.ts index f626ba6..c9c44ce 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -110,7 +110,7 @@ function drawAnnotation(settings: Settings, annotation: AnnotationPos): string { } function drawArrow(settings: Settings, arrow: ArrowPos): string { - const { x1, y1, x2, y2, arrowType, label } = arrow; + const { x1, y1, x2, y2, arrowType, thickness, label } = arrow; const dx = x2 - x1; const dy = y2 - y1; @@ -207,13 +207,13 @@ function drawArrow(settings: Settings, arrow: ArrowPos): string { case "thick": arrowsvg = ` - + - - - + + + `; break; @@ -240,7 +240,7 @@ function drawArrow(settings: Settings, arrow: ArrowPos): string { const lx = mx - px * offset; const ly = (arrowType === "thick") - ? my + (direction * py * settings.thickArrowThickness / 2) + ? my + (direction * py * thickness / 2) : my - py * offset; From e9ba3820727d711073431da76d9561214fa9dc28 Mon Sep 17 00:00:00 2001 From: ys_teng <58208381+YeeShin504@users.noreply.github.com> Date: Sun, 22 Mar 2026 09:47:28 +0800 Subject: [PATCH 2/8] Improve styling and layout of time axis --- docs/editor.css | 86 ++++++++++++-------- docs/editor.js | 58 +++++++------- docs/example.svg | 202 +++++++++++++++++++++++------------------------ src/layout.ts | 41 +++++++--- src/parser.ts | 8 +- 5 files changed, 217 insertions(+), 178 deletions(-) diff --git a/docs/editor.css b/docs/editor.css index d089dbd..5e90e9a 100644 --- a/docs/editor.css +++ b/docs/editor.css @@ -4,18 +4,31 @@ box-sizing: border-box; } +:root { + --bg-main: #1e1e1e; + --bg-sidebar: #252526; + --bg-header: #2d2d2d; + --border-color: #3e3e42; + --text-main: #d4d4d4; + --text-muted: #858585; + --text-header: #969696; + --accent-color: #0e639c; + --accent-hover: #1177bb; + --header-height: 42px; +} + body { font-family: system-ui, -apple-system, sans-serif; - background: #1e1e1e; - color: #d4d4d4; + background: var(--bg-main); + color: var(--text-main); height: 100vh; overflow: hidden; } header { - background: #252526; + background: var(--bg-sidebar); padding: 1rem 2rem; - border-bottom: 1px solid #3e3e42; + border-bottom: 1px solid var(--border-color); } .header-content { @@ -26,17 +39,17 @@ header { .resizer { width: 6px; - background: #1e1e1e; + background: var(--bg-main); cursor: col-resize; position: relative; z-index: 100; - border-left: 1px solid #3e3e42; - border-right: 1px solid #3e3e42; + border-left: 1px solid var(--border-color); + border-right: 1px solid var(--border-color); transition: background 0.2s; } .resizer:hover, .resizer.dragging { - background: #0e639c; + background: var(--accent-color); } .toggle-arrow { @@ -44,9 +57,9 @@ header { top: 50%; left: 50%; transform: translate(-50%, -50%); - background: #2d2d2d; - border: 1px solid #3e3e42; - color: #858585; + background: var(--bg-header); + border: 1px solid var(--border-color); + color: var(--text-muted); width: 20px; height: 40px; padding: 0; @@ -74,7 +87,7 @@ header { #panel-reference { flex: 0 0 280px; - background: #252526; + background: var(--bg-sidebar); transition: flex-basis 0.3s ease-in-out; } @@ -109,21 +122,21 @@ main { } .reference-content { - background: #252526; + background: var(--bg-sidebar); padding: 1.25rem; overflow-y: auto; } .search-container { - padding: 0.75rem 1.25rem; - background: #252526; - border-bottom: 1px solid #3e3e42; + padding: 0.6rem 1rem; + background: var(--bg-header); + border-bottom: 1px solid var(--border-color); } #refSearch { width: 100%; background: #3c3c3c; - border: 1px solid #3e3e42; + border: 1px solid var(--border-color); color: #cccccc; padding: 0.4rem 0.75rem; border-radius: 3px; @@ -133,7 +146,7 @@ main { } #refSearch:focus { - border-color: #0e639c; + border-color: var(--accent-color); } .loading { @@ -156,8 +169,8 @@ main { } .ref-item { - background: #2d2d2d; - border: 1px solid #3e3e42; + background: var(--bg-header); + border: 1px solid var(--border-color); border-radius: 6px; padding: 0.75rem; margin-bottom: 0.75rem; @@ -185,13 +198,13 @@ main { color: #ce9178; border-radius: 4px; margin-bottom: 0.6rem; - border: 1px solid #3e3e42; + border: 1px solid var(--border-color); } .try-btn-small { background: transparent; - border: 1px solid #3e3e42; - color: #858585; + border: 1px solid var(--border-color); + color: var(--text-muted); padding: 0.25rem 0.6rem; border-radius: 3px; font-size: 0.7rem; @@ -211,22 +224,25 @@ main { } .panel-header { - background: #2d2d2d; - padding: 0.75rem 1rem; - border-bottom: 1px solid #3e3e42; + background: var(--bg-header); + padding: 0.6rem 1rem; + border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; + height: var(--header-height); } .panel-title { - font-size: 0.875rem; - font-weight: 500; - color: #cccccc; + font-size: 0.75rem; + font-weight: 600; + color: var(--text-header); + text-transform: uppercase; + letter-spacing: 0.5px; } .copy-btn { - background: #0e639c; + background: var(--accent-color); color: white; border: none; padding: 0.4rem 0.75rem; @@ -237,7 +253,7 @@ main { } .copy-btn:hover { - background: #1177bb; + background: var(--accent-hover); } .copy-btn:active { @@ -306,8 +322,8 @@ svg { } .CodeMirror-gutters { - background: #252526; - border-right: 1px solid #3e3e42; + background: var(--bg-sidebar); + border-right: 1px solid var(--border-color); } .CodeMirror-linenumber { @@ -395,13 +411,13 @@ svg { list-style: none; margin: 0; padding: 2px; - background: #252526; + background: var(--bg-sidebar); border: 1px solid #454545; border-radius: 4px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); font-family: 'JetBrains Mono', 'Consolas', 'Monaco', monospace; font-size: 13px; - color: #d4d4d4; + color: var(--text-main); max-height: 200px; overflow-y: auto; } diff --git a/docs/editor.js b/docs/editor.js index e1e82a6..5df12b4 100644 --- a/docs/editor.js +++ b/docs/editor.js @@ -20,14 +20,14 @@ function initResizer(resizer, leftPanel, rightPanel, isFirst) { resizer.addEventListener('mousedown', (e) => { if (e.target.id === 'toggleReference') return; - + startX = e.clientX; startWidthLeft = leftPanel.offsetWidth; startWidthRight = rightPanel.offsetWidth; - + // Disable transitions during resizing to prevent lag leftPanel.style.transition = 'none'; - + const onMouseMove = (e) => { const deltaX = e.clientX - startX; if (isFirst) { @@ -46,10 +46,10 @@ function initResizer(resizer, leftPanel, rightPanel, isFirst) { document.removeEventListener('mouseup', onMouseUp); resizer.classList.remove('dragging'); document.body.style.cursor = 'default'; - + // Re-enable transition after resizing leftPanel.style.transition = ''; - + if (editor) editor.refresh(); }; @@ -72,10 +72,10 @@ const REFERENCE_DATA = [ { group: "Participants", items: [ - { title: "Define Participant", syntax: 'participant Name alias', example: 'participant Client c\nparticipant Server s' }, - { title: "Participant Spacing", syntax: 'def participantSpacing 240px', example: 'def participantSpacing 300px' }, - { title: "Label Height", syntax: 'def participantLabelHeight 30px', example: 'def participantLabelHeight 50px' }, - { title: "Font Size", syntax: 'def participantFontSize 20px', example: 'def participantFontSize 24px' } + { title: "Define Participant", syntax: 'participant Name alias', example: 'participant Client c' }, + { title: "Participant Spacing", syntax: 'def participantSpacing 240px', example: 'def participantSpacing 240px' }, + { title: "Label Height", syntax: 'def participantLabelHeight 30px', example: 'def participantLabelHeight 30px' }, + { title: "Font Size", syntax: 'def participantFontSize 20px', example: 'def participantFontSize 20px' } ] }, { @@ -92,20 +92,20 @@ const REFERENCE_DATA = [ { group: "Arrow Styles", items: [ - { title: "Arrow Head Size", syntax: 'def arrowHeadSize 10px', example: 'def arrowHeadSize 15px' }, - { title: "Thick Ratio", syntax: 'def thickArrowThickness 1.0', example: 'def thickArrowThickness 0.5' }, - { title: "Label Offset", syntax: 'def labelOffset 10px', example: 'def labelOffset 20px' }, - { title: "Message Font Size", syntax: 'def messageFontSize 15px', example: 'def messageFontSize 18px' } + { title: "Arrow Head Size", syntax: 'def arrowHeadSize 10px', example: 'def arrowHeadSize 10px' }, + { title: "Thick Ratio", syntax: 'def thickArrowThickness 1.0', example: 'def thickArrowThickness 1.0' }, + { title: "Label Offset", syntax: 'def labelOffset 10px', example: 'def labelOffset 10px' }, + { title: "Message Font Size", syntax: 'def messageFontSize 15px', example: 'def messageFontSize 15px' } ] }, { group: "Corrupt & Dropped", items: [ - { title: "Corrupt Start %", syntax: 'def corruptStartRatio 85%', example: 'def corruptStartRatio 50%' }, - { title: "Drop Start %", syntax: 'def dropStartRatio 85%', example: 'def dropStartRatio 50%' }, - { title: "Drop Cross Size", syntax: 'def dropCrossSize 12px', example: 'def dropCrossSize 20px' }, - { title: "Corrupt Squiggle Size", syntax: 'def squiggleSize 20px', example: 'def squiggleSize 10px' }, - { title: "Corrupt Squiggle Count", syntax: 'def squiggleCount 2', example: 'def squiggleCount 5' } + { title: "Corrupt Start %", syntax: 'def corruptStartRatio 85%', example: 'def corruptStartRatio 85%' }, + { title: "Drop Start %", syntax: 'def dropStartRatio 85%', example: 'def dropStartRatio 85%' }, + { title: "Drop Cross Size", syntax: 'def dropCrossSize 12px', example: 'def dropCrossSize 12px' }, + { title: "Corrupt Squiggle Size", syntax: 'def squiggleSize 20px', example: 'def squiggleSize 20px' }, + { title: "Corrupt Squiggle Count", syntax: 'def squiggleCount 2', example: 'def squiggleCount 2' } ] }, { @@ -113,7 +113,7 @@ const REFERENCE_DATA = [ items: [ { title: "Left Note", syntax: 'a < "text"', example: 'a < "Local check"' }, { title: "Right Note", syntax: 'a > "text"', example: 'a > "Processing"' }, - { title: "Annotation Width", syntax: 'def annotationWidth 80px', example: 'def annotationWidth 120px' } + { title: "Annotation Width", syntax: 'def annotationWidth 80px', example: 'def annotationWidth 80px' } ] }, { @@ -121,15 +121,15 @@ const REFERENCE_DATA = [ items: [ { title: "Show Grid", syntax: 'def showGrid true', example: 'def showGrid true' }, { title: "Show Time Ticks", syntax: 'def showTimeTicks true', example: 'def showTimeTicks true' }, - { title: "Time Interval", syntax: 'def timeTickInterval 40px', example: 'def timeTickInterval 60px' }, + { title: "Time Interval", syntax: 'def timeTickInterval 40px', example: 'def timeTickInterval 40px' }, { title: "Time Unit", syntax: 'def timeUnit /ms', example: 'def timeUnit /ms' } ] }, { group: "Layout", items: [ - { title: "Horizontal Padding", syntax: 'def paddingX 30px', example: 'def paddingX 50px' }, - { title: "Vertical Padding", syntax: 'def paddingY 20px', example: 'def paddingY 50px' } + { title: "Horizontal Padding", syntax: 'def paddingX 30px', example: 'def paddingX 30px' }, + { title: "Vertical Padding", syntax: 'def paddingY 20px', example: 'def paddingY 20px' } ] } ]; @@ -142,8 +142,8 @@ function renderReference(filter = '') { const query = filter.toLowerCase().trim(); REFERENCE_DATA.forEach(group => { - const filteredItems = group.items.filter(item => - item.title.toLowerCase().includes(query) || + const filteredItems = group.items.filter(item => + item.title.toLowerCase().includes(query) || item.syntax.toLowerCase().includes(query) || group.group.toLowerCase().includes(query) ); @@ -153,23 +153,23 @@ function renderReference(filter = '') { const groupEl = document.createElement('div'); groupEl.className = 'ref-group'; groupEl.innerHTML = `

${group.group}

`; - + const itemsContainer = document.createElement('div'); itemsContainer.className = 'group-items'; filteredItems.forEach(item => { const itemEl = document.createElement('div'); itemEl.className = 'ref-item'; - + const titleEl = document.createElement('div'); titleEl.className = 'ref-title'; titleEl.textContent = item.title; - + const codeEl = document.createElement('code'); codeEl.className = 'ref-syntax'; // Use the editor engine to highlight this static snippet! CodeMirror.runMode(item.syntax, 'protocol-ml', codeEl); - + const btn = document.createElement('button'); btn.className = 'try-btn-small'; btn.textContent = 'Try it'; @@ -228,7 +228,7 @@ b =>[1.5] a : "thicker reply" a @2 < "left label @2" b @5 > "right label @5"`; -CodeMirror.commands.autocomplete = function(cm) { +CodeMirror.commands.autocomplete = function (cm) { cm.showHint({ hint: CodeMirror.hint["protocol-ml"] }); }; diff --git a/docs/example.svg b/docs/example.svg index fa4c471..d54d429 100644 --- a/docs/example.svg +++ b/docs/example.svg @@ -1,19 +1,19 @@ - + - - - - - - - - - - - + + + + + + + + + + + Time /ms - - + + - + - + - + - + - + - + - + - + - + - + Alice - + Bob - + - - - + + + @@ -212,17 +212,17 @@ - - - + + + @@ -230,19 +230,19 @@ - - - - - + + + + + @@ -250,19 +250,19 @@ - - - - - + + + + + @@ -270,17 +270,17 @@ - - - + + + @@ -288,17 +288,17 @@ - - - + + + @@ -306,47 +306,47 @@ - - - - - - - + + + + + + + - thick + thick reply - - - - - - - + + + + + + + - thick reply + thicker reply - left -label -@2 + left +label +@2 - right -label -@5 + right +label +@5 \ No newline at end of file diff --git a/src/layout.ts b/src/layout.ts index ca49949..8936113 100644 --- a/src/layout.ts +++ b/src/layout.ts @@ -50,14 +50,37 @@ export interface Diagram { export function resolveLayout(entities: Entities): Diagram { const { settings, participants, actions, numAnnotations } = entities; - // 1. check if need extra spacing for annotations - let timeTickMargin = settings.showTimeTicks ? 60 : 0; - let annMargin = numAnnotations > 0 ? settings.annotationWidth : 0; + // 1. Calculate horizontal positions + const timeUnitLabel = settings.showTimeTicks ? `Time ${settings.timeUnit}` : ""; + const timeAxisHeaderHalfWidth = timeUnitLabel.length * settings.participantFontSize * 0.3; - let timeAxisX = settings.paddingX + timeTickMargin; - let startX = timeAxisX + (settings.showTimeTicks ? 20 : 0) + annMargin; + const firstParticipantName = participants[0]?.name || ""; + const firstParticipantHalfWidth = firstParticipantName.length * settings.participantFontSize * 0.3; - let width = startX + settings.participantSpacing * (participants.length - 1) + annMargin + settings.paddingX; + // Distance from the time axis header label to the left is fixed at paddingX. + // We use max(halfWidth, 40) to also leave room for tick labels like "100" if the unit is short. + const timeAxisX = settings.paddingX + Math.max(timeAxisHeaderHalfWidth, 40); + + const textBuffer = 20; // minimal gap between text labels + const headerOverlapGap = timeAxisHeaderHalfWidth + firstParticipantHalfWidth + textBuffer; + const annSpace = numAnnotations > 0 ? (settings.annotationWidth + settings.labelOffset) : 0; + + // Distance from axis line to first lifeline. + const axisLineToParticipantGap = settings.showTimeTicks + ? Math.max(headerOverlapGap, annSpace) + : (20 + annSpace); + + const startX = settings.showTimeTicks + ? timeAxisX + axisLineToParticipantGap + : settings.paddingX + axisLineToParticipantGap; + + const lastParticipantX = startX + settings.participantSpacing * (participants.length - 1); + const lastParticipantName = participants[participants.length - 1]?.name || ""; + const lastParticipantHalfWidth = lastParticipantName.length * settings.participantFontSize * 0.3; + + // Total width calculation + const rightBuffer = Math.max(lastParticipantHalfWidth, annSpace); + const width = lastParticipantX + rightBuffer + settings.paddingX; // 2. figure out top spacing for participant label and padding @@ -86,13 +109,13 @@ export function resolveLayout(entities: Entities): Diagram { let startY = counter; let endY = counter + 1; - if (action.start) { + if (action.start !== undefined) { counter = action.start; startY = counter; endY = counter + 1; } - if (action.end) { + if (action.end !== undefined) { endY = action.end; } @@ -120,7 +143,7 @@ export function resolveLayout(entities: Entities): Diagram { case "annotation": // @ positioning should just position and nothing else let y = counter; - if (action.height) { + if (action.height !== undefined) { y = action.height; } diff --git a/src/parser.ts b/src/parser.ts index a686eca..db5dd21 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -215,7 +215,7 @@ export function parse(src: string): Entities { // arrows const arrowMatch = line.match( - /^(\w+)(?:\s*@([\d.%px]+))?\s*(->|=>|~>|-x)(?:\[([\d.]+)\])?\s*(\w+)(?:\s*@([\d.%px]+))?(?:\s*:\s*"(.+)")?/ + /^(\w+)(?:\s*@([\d.%px]+))?\s*(->|=>|~>|-x)(?:\[([\d.]+)\])?\s*(?:@([\d.%px]+)\s+)?(\w+)(?:\s*@([\d.%px]+))?(?:\s*:\s*"(.+)")?/ ); if (arrowMatch) { @@ -233,9 +233,9 @@ export function parse(src: string): Entities { start: arrowMatch[2] ? (parseNumber(arrowMatch[2]) ?? undefined) : undefined, arrowType: typeMap[arrowMatch[3]], thicknessRatio: arrowMatch[4] ? parseFloat(arrowMatch[4]) : undefined, - to: arrowMatch[5], - end: arrowMatch[6] ? (parseNumber(arrowMatch[6]) ?? undefined) : undefined, - label: arrowMatch[7] + to: arrowMatch[6], + end: (arrowMatch[5] || arrowMatch[7]) ? (parseNumber(arrowMatch[5] || arrowMatch[7]) ?? undefined) : undefined, + label: arrowMatch[8] }); continue; From 69e5048aa71d3b5c29c41d546914aae7a41f1cc3 Mon Sep 17 00:00:00 2001 From: ys_teng <58208381+YeeShin504@users.noreply.github.com> Date: Sun, 22 Mar 2026 14:13:38 +0800 Subject: [PATCH 3/8] Add time tick step setting --- docs/editor.css | 6 ++++-- docs/editor.js | 1 + docs/syntax-highlight.js | 6 +++--- src/layout.ts | 3 ++- src/parser.ts | 2 ++ 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/editor.css b/docs/editor.css index 5e90e9a..dce8661 100644 --- a/docs/editor.css +++ b/docs/editor.css @@ -48,7 +48,8 @@ header { transition: background 0.2s; } -.resizer:hover, .resizer.dragging { +.resizer:hover, +.resizer.dragging { background: var(--accent-color); } @@ -69,7 +70,7 @@ header { cursor: pointer; font-size: 10px; border-radius: 4px; - box-shadow: 0 2px 4px rgba(0,0,0,0.2); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); transition: all 0.2s; } @@ -358,6 +359,7 @@ svg { .cm-pml-alias { color: #4ec9b0; } + .cm-pml-arrow-normal { color: #4fc1ff; font-weight: bold; diff --git a/docs/editor.js b/docs/editor.js index 5df12b4..f3a4565 100644 --- a/docs/editor.js +++ b/docs/editor.js @@ -122,6 +122,7 @@ const REFERENCE_DATA = [ { title: "Show Grid", syntax: 'def showGrid true', example: 'def showGrid true' }, { title: "Show Time Ticks", syntax: 'def showTimeTicks true', example: 'def showTimeTicks true' }, { title: "Time Interval", syntax: 'def timeTickInterval 40px', example: 'def timeTickInterval 40px' }, + { title: "Time Tick Step", syntax: 'def timeTickStep 5', example: 'def timeTickStep 5' }, { title: "Time Unit", syntax: 'def timeUnit /ms', example: 'def timeUnit /ms' } ] }, diff --git a/docs/syntax-highlight.js b/docs/syntax-highlight.js index 8d779d2..13f7c55 100644 --- a/docs/syntax-highlight.js +++ b/docs/syntax-highlight.js @@ -1,10 +1,10 @@ -const KEYWORDS = ["def", "participant", "true", "false"]; +const KEYWORDS = ["def", "participant", "true", "false"]; const SETTINGS = [ "arrowHeadSize", "dropCrossSize", "thickArrowThickness", "labelOffset", "corruptStartRatio", "dropStartRatio", "squiggleSize", "squiggleCount", "timeTickInterval", "participantSpacing", "participantLabelHeight", "annotationWidth", "participantFontSize", "messageFontSize", - "paddingX", "paddingY", "showGrid", "showTimeTicks", "timeUnit" + "paddingX", "paddingY", "showGrid", "showTimeTicks", "timeTickStep", "timeUnit" ]; const KEYWORDS_REGEX = new RegExp(`\\b(${KEYWORDS.join('|')})\\b`); @@ -111,7 +111,7 @@ CodeMirror.hint["protocol-ml"] = function (cm) { const bIsKeyword = KEYWORDS.includes(b); if (aIsKeyword && !bIsKeyword) return -1; if (!aIsKeyword && bIsKeyword) return 1; - + return a.localeCompare(b); }); diff --git a/src/layout.ts b/src/layout.ts index 8936113..8384f0e 100644 --- a/src/layout.ts +++ b/src/layout.ts @@ -183,7 +183,8 @@ export function resolveLayout(entities: Entities): Diagram { }); if (settings.showTimeTicks || settings.showGrid) { - for (let i = 0; i <= totalTicks; i++) { + const step = Math.max(1, settings.timeTickStep); + for (let i = 0; i <= totalTicks; i += step) { draws.push({ type: "tick", y: oldHeight + settings.timeTickInterval + i * settings.timeTickInterval, diff --git a/src/parser.ts b/src/parser.ts index db5dd21..dfcfef1 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -60,6 +60,7 @@ export interface Settings { showGrid: boolean; showTimeTicks: boolean; + timeTickStep: number; timeUnit: string; } @@ -87,6 +88,7 @@ const DEFAULT_SETTINGS: Settings = { showGrid: false, showTimeTicks: false, + timeTickStep: 1, timeUnit: "", }; From f58254c758bf63b6e3e37cdaead343ddb2975209 Mon Sep 17 00:00:00 2001 From: ys_teng <58208381+YeeShin504@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:35:25 +0800 Subject: [PATCH 4/8] Add variable width for thick arrows --- docs/editor.js | 7 +++---- src/layout.ts | 20 +++++++++++++------- src/parser.ts | 32 +++++++++++++++++++++++++------- src/renderer.ts | 29 +++++++++++++++++++++++------ 4 files changed, 64 insertions(+), 24 deletions(-) diff --git a/docs/editor.js b/docs/editor.js index f3a4565..32bbb1d 100644 --- a/docs/editor.js +++ b/docs/editor.js @@ -82,11 +82,10 @@ const REFERENCE_DATA = [ group: "Message Arrows", items: [ { title: "Normal Arrow", syntax: 'a -> b : "label"', example: 'a -> b : "Request"' }, - { title: "Thick Arrow", syntax: 'a => b : "label"', example: 'a => b : "Big Data"' }, - { title: "Variable Thickness", syntax: 'a =>[thickness] b : "label"', example: 'a =>[0.5] b : "Thin"' }, + { title: "Variable Thick Arrow", syntax: 'a [start]=>[end] b : "label"', example: 'a [1.5]=> b : "Big Data"' }, { title: "Corrupt Arrow", syntax: 'a ~> b : "label"', example: 'a ~> b : "Fragmented"' }, { title: "Dropped Message", syntax: 'a -x b : "label"', example: 'a -x b : "Timeout"' }, - { title: "Time Offsets", syntax: 'a @1.5 -> b @1', example: 'a @1.5 -> b @1 : "Relative Time"' } + { title: "Time Offsets", syntax: 'a @1.5 -> b @1', example: 'a @1.5 -> b @1 : "Time Travel"' } ] }, { @@ -224,7 +223,7 @@ a -x b : "dropped" b -x a : "dropped reply" a => b : "thick reply" -b =>[1.5] a : "thicker reply" +b =>[1.5] a : "thicker reply (end 1.5)" a @2 < "left label @2" b @5 > "right label @5"`; diff --git a/src/layout.ts b/src/layout.ts index 8384f0e..75b2d38 100644 --- a/src/layout.ts +++ b/src/layout.ts @@ -15,7 +15,8 @@ export interface ArrowPos { x2: number; y2: number; arrowType: ArrowType; - thickness: number; + thicknessStart: number; + thicknessEnd: number; label?: string; } @@ -119,11 +120,15 @@ export function resolveLayout(entities: Entities): Diagram { endY = action.end; } - const currentThicknessRatio = (action.arrowType === "thick") - ? (action.thicknessRatio ?? settings.thickArrowThickness) + const startRatio = (action.arrowType === "thick") + ? (action.thicknessStart ?? settings.thickArrowThickness) : 0; - - const thickness = currentThicknessRatio * settings.timeTickInterval; + const endRatio = (action.arrowType === "thick") + ? (action.thicknessEnd ?? settings.thickArrowThickness) + : 0; + + const thicknessStart = startRatio * settings.timeTickInterval; + const thicknessEnd = endRatio * settings.timeTickInterval; draws.push({ type: action.type, @@ -132,11 +137,12 @@ export function resolveLayout(entities: Entities): Diagram { x2: participantsX.get(action.to), y2: height + endY * settings.timeTickInterval, arrowType: action.arrowType, - thickness, + thicknessStart, + thicknessEnd, label: action.label, }); - counterMax = Math.max(counterMax, startY + currentThicknessRatio, endY + currentThicknessRatio); + counterMax = Math.max(counterMax, startY + startRatio, endY + endRatio); counter++; break; diff --git a/src/parser.ts b/src/parser.ts index dfcfef1..62cf70f 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -17,7 +17,8 @@ export interface Arrow { start?: number; end?: number; arrowType: ArrowType; - thicknessRatio?: number; + thicknessStart?: number; + thicknessEnd?: number; label?: string; } @@ -217,7 +218,7 @@ export function parse(src: string): Entities { // arrows const arrowMatch = line.match( - /^(\w+)(?:\s*@([\d.%px]+))?\s*(->|=>|~>|-x)(?:\[([\d.]+)\])?\s*(?:@([\d.%px]+)\s+)?(\w+)(?:\s*@([\d.%px]+))?(?:\s*:\s*"(.+)")?/ + /^(\w+)(?:\s*@([\d.%px]+))?\s*(?:\[([\d.]+)\])?\s*(->|=>|~>|-x)(?:\[([\d.]+)\])?\s*(?:@([\d.%px]+)\s+)?(\w+)(?:\s*@([\d.%px]+))?(?:\s*:\s*"(.+)")?/ ); if (arrowMatch) { @@ -229,15 +230,32 @@ export function parse(src: string): Entities { "-x": "dropped" }; + const type = typeMap[arrowMatch[4]]; + let tStart = arrowMatch[3] ? parseFloat(arrowMatch[3]) : undefined; + let tEnd = arrowMatch[5] ? parseFloat(arrowMatch[5]) : undefined; + + // Rule: + // [start]=> -> end=start + // =>[end] -> start=1 + // [start]=>[end] -> start=start, end=end + if (type === "thick") { + if (tStart !== undefined && tEnd === undefined) { + tEnd = tStart; + } else if (tEnd !== undefined && tStart === undefined) { + tStart = 1.0; + } + } + actions.push({ type: "arrow", from: arrowMatch[1], start: arrowMatch[2] ? (parseNumber(arrowMatch[2]) ?? undefined) : undefined, - arrowType: typeMap[arrowMatch[3]], - thicknessRatio: arrowMatch[4] ? parseFloat(arrowMatch[4]) : undefined, - to: arrowMatch[6], - end: (arrowMatch[5] || arrowMatch[7]) ? (parseNumber(arrowMatch[5] || arrowMatch[7]) ?? undefined) : undefined, - label: arrowMatch[8] + arrowType: type, + thicknessStart: tStart, + thicknessEnd: tEnd, + to: arrowMatch[7], + end: (arrowMatch[6] || arrowMatch[8]) ? (parseNumber(arrowMatch[6] || arrowMatch[8]) ?? undefined) : undefined, + label: arrowMatch[9] }); continue; diff --git a/src/renderer.ts b/src/renderer.ts index c9c44ce..54cd1cf 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -110,7 +110,7 @@ function drawAnnotation(settings: Settings, annotation: AnnotationPos): string { } function drawArrow(settings: Settings, arrow: ArrowPos): string { - const { x1, y1, x2, y2, arrowType, thickness, label } = arrow; + const { x1, y1, x2, y2, arrowType, thicknessStart, thicknessEnd, label } = arrow; const dx = x2 - x1; const dy = y2 - y1; @@ -205,15 +205,32 @@ function drawArrow(settings: Settings, arrow: ArrowPos): string { break; case "thick": + const dx_b = x2 - x1; + const dy_b = (y2 + thicknessEnd) - (y1 + thicknessStart); + const len_b = Math.hypot(dx_b, dy_b); + const ux_b = dx_b / len_b; + const uy_b = dy_b / len_b; + const px_b = -uy_b; + const py_b = ux_b; + + const baseX_b = x2 - ux_b * settings.arrowHeadSize; + const baseY_b = (y2 + thicknessEnd) - uy_b * settings.arrowHeadSize; + + const leftX_b = baseX_b + px_b * settings.arrowHeadSize * 0.5; + const leftY_b = baseY_b + py_b * settings.arrowHeadSize * 0.5; + + const rightX_b = baseX_b - px_b * settings.arrowHeadSize * 0.5; + const rightY_b = baseY_b - py_b * settings.arrowHeadSize * 0.5; + arrowsvg = ` - + - - - + + + `; break; @@ -240,7 +257,7 @@ function drawArrow(settings: Settings, arrow: ArrowPos): string { const lx = mx - px * offset; const ly = (arrowType === "thick") - ? my + (direction * py * thickness / 2) + ? my + (direction * py * (thicknessStart + thicknessEnd) / 4) : my - py * offset; From b1774b3fa793dda96bf6b0ee42d40df2e3ffe5d5 Mon Sep 17 00:00:00 2001 From: ys_teng <58208381+YeeShin504@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:30:07 +0800 Subject: [PATCH 5/8] Update github actions to use node 24 --- .github/workflows/deploy-editor.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-editor.yml b/.github/workflows/deploy-editor.yml index 6d829c2..6270844 100644 --- a/.github/workflows/deploy-editor.yml +++ b/.github/workflows/deploy-editor.yml @@ -11,12 +11,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: - node-version: "24" + node-version: 24 + cache: npm - name: Install dependencies run: npm install From d73e2ba5142121f0bbee845bd48bc4bb740891d2 Mon Sep 17 00:00:00 2001 From: ys_teng <58208381+YeeShin504@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:48:58 +0800 Subject: [PATCH 6/8] Use modern github actions to upload and deploy page --- .github/workflows/deploy-editor.yml | 35 ++++++++++++++--------------- package.json | 2 +- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/.github/workflows/deploy-editor.yml b/.github/workflows/deploy-editor.yml index 6270844..5eb1e47 100644 --- a/.github/workflows/deploy-editor.yml +++ b/.github/workflows/deploy-editor.yml @@ -6,6 +6,11 @@ on: - master - dev +permissions: + contents: read + pages: write + id-token: write + jobs: deploy: runs-on: ubuntu-latest @@ -23,23 +28,17 @@ jobs: run: npm install - name: Build interactive editor - run: npm run build:docs + run: | + if [ "${{ github.ref_name }}" == "dev" ]; then + npm run build:docs -- --dist-dir dist/dev + else + npm run build:docs -- --dist-dir dist + fi - - name: Deploy to GitHub Pages (master) - if: github.ref == 'refs/heads/master' - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: dist - publish_branch: gh-pages - keep_files: true - - - name: Deploy to GitHub Pages (dev) - if: github.ref == 'refs/heads/dev' - uses: peaceiris/actions-gh-pages@v3 + - name: Upload artifact + uses: actions/upload-artifact@v4 with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: dist - destination_dir: dev - publish_branch: gh-pages - keep_files: true + path: ./dist + + - name: Deploy to GitHub Pages + uses: actions/deploy-pages@v4 diff --git a/package.json b/package.json index 8d05a01..0744154 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "dev": "parcel index.html", "dev:docs": "parcel docs/index.html", "build": "tsc", - "build:docs": "parcel build docs/index.html --dist-dir dist --public-url ./" + "build:docs": "parcel build docs/index.html --public-url ./" }, "targets": { "module": false, From 126ff2fe22f715af9881cc5d2897687e63f9927a Mon Sep 17 00:00:00 2001 From: ys_teng <58208381+YeeShin504@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:50:52 +0800 Subject: [PATCH 7/8] Change update name --- .github/workflows/deploy-editor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-editor.yml b/.github/workflows/deploy-editor.yml index 5eb1e47..b2f1415 100644 --- a/.github/workflows/deploy-editor.yml +++ b/.github/workflows/deploy-editor.yml @@ -36,7 +36,7 @@ jobs: fi - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-pages-artifact@v4 with: path: ./dist From 791ec42b7b0257b5c100488a64ba32e5f4c7cfb4 Mon Sep 17 00:00:00 2001 From: ys_teng <58208381+YeeShin504@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:56:54 +0800 Subject: [PATCH 8/8] Fix github actions --- .github/workflows/deploy-editor.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-editor.yml b/.github/workflows/deploy-editor.yml index b2f1415..7daaa57 100644 --- a/.github/workflows/deploy-editor.yml +++ b/.github/workflows/deploy-editor.yml @@ -12,7 +12,7 @@ permissions: id-token: write jobs: - deploy: + build: runs-on: ubuntu-latest steps: - name: Checkout repository @@ -39,6 +39,15 @@ jobs: uses: actions/upload-pages-artifact@v4 with: path: ./dist + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + + steps: - name: Deploy to GitHub Pages + id: deployment uses: actions/deploy-pages@v4