diff --git a/.github/workflows/deploy-editor.yml b/.github/workflows/deploy-editor.yml index 6d829c2..7daaa57 100644 --- a/.github/workflows/deploy-editor.yml +++ b/.github/workflows/deploy-editor.yml @@ -6,39 +6,48 @@ on: - master - dev +permissions: + contents: read + pages: write + id-token: write + jobs: - deploy: + build: 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 - 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 + - name: Upload artifact + uses: actions/upload-pages-artifact@v4 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 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: dist - destination_dir: dev - publish_branch: gh-pages - keep_files: true + 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 diff --git a/docs/editor.css b/docs/editor.css index d089dbd..dce8661 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,18 @@ 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; +.resizer:hover, +.resizer.dragging { + background: var(--accent-color); } .toggle-arrow { @@ -44,9 +58,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; @@ -56,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; } @@ -74,7 +88,7 @@ header { #panel-reference { flex: 0 0 280px; - background: #252526; + background: var(--bg-sidebar); transition: flex-basis 0.3s ease-in-out; } @@ -109,21 +123,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 +147,7 @@ main { } #refSearch:focus { - border-color: #0e639c; + border-color: var(--accent-color); } .loading { @@ -156,8 +170,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 +199,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 +225,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 +254,7 @@ main { } .copy-btn:hover { - background: #1177bb; + background: var(--accent-hover); } .copy-btn:active { @@ -306,8 +323,8 @@ svg { } .CodeMirror-gutters { - background: #252526; - border-right: 1px solid #3e3e42; + background: var(--bg-sidebar); + border-right: 1px solid var(--border-color); } .CodeMirror-linenumber { @@ -342,6 +359,7 @@ svg { .cm-pml-alias { color: #4ec9b0; } + .cm-pml-arrow-normal { color: #4fc1ff; font-weight: bold; @@ -395,13 +413,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 55674a0..32bbb1d 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,39 +72,39 @@ 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' } ] }, { 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 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"' } ] }, { 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: "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' } ] }, { @@ -112,7 +112,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' } ] }, { @@ -120,15 +120,16 @@ 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 Tick Step", syntax: 'def timeTickStep 5', example: 'def timeTickStep 5' }, { 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' } ] } ]; @@ -141,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) ); @@ -152,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'; @@ -221,13 +222,13 @@ 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 (end 1.5)" 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/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/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, diff --git a/src/layout.ts b/src/layout.ts index 5d5aea9..75b2d38 100644 --- a/src/layout.ts +++ b/src/layout.ts @@ -15,6 +15,8 @@ export interface ArrowPos { x2: number; y2: number; arrowType: ArrowType; + thicknessStart: number; + thicknessEnd: number; label?: string; } @@ -49,14 +51,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 @@ -85,16 +110,26 @@ 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; } + const startRatio = (action.arrowType === "thick") + ? (action.thicknessStart ?? settings.thickArrowThickness) + : 0; + 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, x1: participantsX.get(action.from), @@ -102,18 +137,19 @@ export function resolveLayout(entities: Entities): Diagram { x2: participantsX.get(action.to), y2: height + endY * settings.timeTickInterval, arrowType: action.arrowType, + thicknessStart, + thicknessEnd, 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 + startRatio, endY + endRatio); + counter++; break; case "annotation": // @ positioning should just position and nothing else let y = counter; - if (action.height) { + if (action.height !== undefined) { y = action.height; } @@ -153,7 +189,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 cb34f01..62cf70f 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -17,6 +17,8 @@ export interface Arrow { start?: number; end?: number; arrowType: ArrowType; + thicknessStart?: number; + thicknessEnd?: number; label?: string; } @@ -59,13 +61,14 @@ export interface Settings { showGrid: boolean; showTimeTicks: boolean; + timeTickStep: number; timeUnit: string; } const DEFAULT_SETTINGS: Settings = { arrowHeadSize: 10, dropCrossSize: 12, - thickArrowThickness: 40, + thickArrowThickness: 1.0, labelOffset: 10, corruptStartRatio: 0.85, dropStartRatio: 0.85, @@ -86,6 +89,7 @@ const DEFAULT_SETTINGS: Settings = { showGrid: false, showTimeTicks: false, + timeTickStep: 1, timeUnit: "", }; @@ -214,7 +218,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*(?:\[([\d.]+)\])?\s*(->|=>|~>|-x)(?:\[([\d.]+)\])?\s*(?:@([\d.%px]+)\s+)?(\w+)(?:\s*@([\d.%px]+))?(?:\s*:\s*"(.+)")?/ ); if (arrowMatch) { @@ -226,14 +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]], - to: arrowMatch[4], - end: arrowMatch[5] ? (parseNumber(arrowMatch[5]) ?? undefined) : undefined, - label: arrowMatch[6] + 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 f626ba6..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, 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 * settings.thickArrowThickness / 2) + ? my + (direction * py * (thicknessStart + thicknessEnd) / 4) : my - py * offset;