Skip to content

Commit

Permalink
Arrowhead support for line segments (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
edemaine committed Nov 14, 2022
1 parent 8fe5ed9 commit 7946839
Show file tree
Hide file tree
Showing 16 changed files with 133 additions and 6 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ instead of version numbers.

## 2022-11-14

* New start/end arrowhead support for line segments
[[#35](https://github.com/edemaine/cocreate/issues/35)]
* Bug fix in rectangular selection of anchors with translated objects.

## 2022-11-13
Expand Down
6 changes: 6 additions & 0 deletions client/AppState.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ export {currentOpacity, setCurrentOpacity}
[currentOpacityOn, setCurrentOpacityOn] = createSignal()
export {currentOpacityOn, setCurrentOpacityOn}

## Initialized in ./tools/arrow.coffee:
[currentArrowStart, setCurrentArrowStart] = createSignal()
export {currentArrowStart, setCurrentArrowStart}
[currentArrowEnd, setCurrentArrowEnd] = createSignal()
export {currentArrowEnd, setCurrentArrowEnd}

## Initialized in ./tools/width.coffee:
[currentWidth, setCurrentWidth] = createSignal()
export {currentWidth, setCurrentWidth}
Expand Down
10 changes: 10 additions & 0 deletions client/DrawApp.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,10 @@ export DrawAppRoom = ->
<ToolCategory category="width" placement="top"/>
</div>
}
{###if currentTool() in ['segment', 'select']###}
<div id="arrows" class="subpalette">
<ToolCategory category="arrow" placement="top"/>
</div>
<div id="opacities" class="subpalette">
<ToolCategory category="opacity" placement="top"/>
{if currentOpacityOn()
Expand All @@ -466,6 +470,12 @@ export DrawAppRoom = ->
<filter id="selectFilter">
<feGaussianBlur stdDeviation="5"/>
</filter>
{### arrowhead from https://developer.mozilla.org/en-US/docs/Web/SVG/Element/marker ###}
<marker id="arrow" viewBox="0 0 10 10" refX="9.5" refY="5"
markerWidth="4" markerHeight="4" orient="auto-start-reverse"
overflow="visible">
<path d="M 0 -1 L 10 3.85405 L 10 6.14595 L 0 11 z"/>
</marker>
</svg>
<svg id="historyBoard" class="board" touch-action="none"
ref={historyBoardRef}/>
Expand Down
12 changes: 12 additions & 0 deletions client/RenderObjects.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ export class RenderObjects
unless (poly = @dom[id])?
@root.appendChild @dom[id] = poly =
dom.create 'polyline', null, dataset: id: id
if obj.arrowStart or obj.arrowEnd
## Dreaming of `context-stroke` when one marker will suffice.
## [https://svgwg.org/svg2-draft/painting.html#TermContextElement]
arrowId = "arrow-#{obj.color}"
unless (document.getElementById arrowId)?
arrow = document.getElementById 'arrow'
coloredArrow = arrow.cloneNode true
coloredArrow.id = arrowId
coloredArrow.firstChild.setAttribute 'fill', obj.color
arrow.parentNode.insertBefore coloredArrow, arrow
dom.attr poly,
points: ("#{x},#{y}" for {x, y} in obj.pts).join ' '
stroke: obj.color
Expand All @@ -100,6 +110,8 @@ export class RenderObjects
'stroke-linecap': 'round'
'stroke-linejoin': 'round'
fill: 'none'
'marker-start': if obj.arrowStart then "url(##{arrowId})"
'marker-end': if obj.arrowEnd then "url(##{arrowId})"
poly
renderRect: (obj) ->
id = @id obj
Expand Down
8 changes: 8 additions & 0 deletions client/Selection.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
## Selection class is for maintaining and highlighted set of selected objects
## (which often come from Highlighter).

import {setCurrentArrowStart, setCurrentArrowEnd} from './AppState'
import {minSvgSize} from './BBox'
import {undoStack} from './UndoStack'
import {gridOffset} from './Grid'
Expand Down Expand Up @@ -78,6 +79,7 @@ export class Highlighter
html = target.outerHTML
#.replace /\bdata-id=["'][^'"]*["']/g, ''
.replace /(\bstroke-width=["'])([\d.]+)(["'])/g, doubler
.replace /(\bmarker-(start|end)=["'])([^"']*)(["'])/g, ''
.replace /(\br=["'])([\d.]+)(["'])/g, doubler
.replace /<image\b/g, '<image filter="url(#selectFilter)"'
if /<text\b/.test html
Expand Down Expand Up @@ -214,6 +216,8 @@ export class Selection
continue unless obj.type in ['rect', 'ellipse']
when 'color'
continue unless obj.type in ['pen', 'poly', 'rect', 'ellipse', 'text']
when 'arrowStart', 'arrowEnd'
continue unless obj.type in ['poly']
obj
return unless objs.length
undoStack.pushAndDo
Expand Down Expand Up @@ -327,5 +331,9 @@ export class Selection
selectOpacityOff true
if (width = uniformAttribute 'width')? # uniform line width
selectWidth width, true, true
if (arrowStart = uniformAttribute 'arrowStart', false) != null
setCurrentArrowStart arrowStart ? null # map undefined (all null) to null
if (arrowEnd = uniformAttribute 'arrowEnd', false) != null
setCurrentArrowEnd arrowEnd ? null # map undefined (all null) to null
if (fontSize = uniformAttribute 'fontSize')? # uniform font size
selectFontSize fontSize, true, true
2 changes: 1 addition & 1 deletion client/main.styl
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ button(activeBorder)
.tool
padding: 2px

.width, .fontSize
.width, .arrow, .fontSize
display: flex
justify-content: center
align-items: center
Expand Down
39 changes: 39 additions & 0 deletions client/tools/arrow.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {defineTool} from './defineTool'
import {selectDrawingTool} from './tools'
import {currentBoard, currentArrowStart, setCurrentArrowStart, currentArrowEnd, setCurrentArrowEnd} from '../AppState'

arrowSize = 26

for {direction, attribute, hotkey, currentArrow, setCurrentArrow} in [
direction: 'start'
attribute: 'arrowStart'
hotkey: '<'
currentArrow: currentArrowStart
setCurrentArrow: setCurrentArrowStart
,
direction: 'end'
attribute: 'arrowEnd'
hotkey: '>'
currentArrow: currentArrowEnd
setCurrentArrow: setCurrentArrowEnd
]
do (direction, attribute, hotkey, currentArrow, setCurrentArrow) ->
defineTool
name: "arrow:#{direction}"
category: 'arrow'
class: 'arrow attrib'
hotkey: hotkey
help: "Toggle arrow at #{direction} of line segments"
active: -> Boolean currentArrow()
click: ->
setCurrentArrow if currentArrow() then null else 'arrow'
if (selection = currentBoard().selection)?.nonempty()
selection.edit attribute, currentArrow()
else
selectDrawingTool()
icon: ->
<svg viewBox="#{-arrowSize} #{-arrowSize/2} #{arrowSize} #{arrowSize}"
width={arrowSize} height={arrowSize}
transform={'rotate(180)' if direction == 'start'}>
<line x1={-22} x2={-4} stroke-width={3} stroke-linecap="round" marker-end="url(#arrow)"/>
</svg>
17 changes: 16 additions & 1 deletion client/tools/download.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,25 @@ export makeSVGSync = ->
elts = board.renderedChildren()
## Compute bounding box using SVG's getBBox() and getCTM()
bbox = board.renderedBBox elts
## Add used arrowhead markers
prepend = []
arrows = new Set
for elt in elts
for attribute in ['marker-start', 'marker-end']
if (marker = elt.getAttribute attribute)
match = marker.match /^url\(#(.+)\)$/
if match?
arrowId = match[1]
unless arrows.has arrowId
arrows.add arrowId
prepend.push document.getElementById arrowId
else
console.warn "Unrecognized #{attribute}: #{marker}"
## Temporarily make grid span entire drawing
if grid?
grid.update bbox
elts.splice 0, 0, grid.grid
prepend.push grid.grid
elts[0...0] = prepend
## Convert everything to SVG
svg = (elt.outerHTML for elt in elts).join '\n'
.replace /&nbsp;/g, '\u00a0' # SVG doesn't support &nbsp;
Expand Down
5 changes: 4 additions & 1 deletion client/tools/modes.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {defineTool} from './defineTool'
import {tryAddImageUrl} from './image'
import {tools, selectTool} from './tools'
import {anchorFromEvent, anchorMove, anchorsOf, rawAnchorsOf} from '../Anchor'
import {currentBoard, mainBoard, currentRoom, currentPage, currentTool, currentColor, currentFill, currentFillOn, currentFontSize, currentOpacity, currentOpacityOn, currentWidth} from '../AppState'
import {currentBoard, mainBoard, currentRoom, currentPage, currentTool, currentArrowStart, currentArrowEnd, currentColor, currentFill, currentFillOn, currentFontSize, currentOpacity, currentOpacityOn, currentWidth} from '../AppState'
import {maybeSnapPointToGrid} from '../Grid'
import {Highlighter, highlighterClear} from '../Selection'
import {undoStack} from '../UndoStack'
Expand Down Expand Up @@ -494,6 +494,9 @@ rectLikeTool = (type, fillable, constrain) ->
width: width
object.fill = currentFill() if fillable and currentFillOn()
object.opacity = currentOpacity() if currentOpacityOn()
if type == 'poly'
object.arrowStart = currentArrowStart() if currentArrowStart()
object.arrowEnd = currentArrowEnd() if currentArrowEnd()
pointers[e.pointerId] =
origin: origin
start: e
Expand Down
1 change: 1 addition & 0 deletions client/tools/tools.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import './links'
import './page'
import './zoom'
import './width'
import './arrow'
import './opacity'
import './font'
import './color'
Expand Down
6 changes: 6 additions & 0 deletions doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,12 @@ the line-width choices are replaced by font-size choices.
These control the size of the entire text object.
Currently, you have seven integral choices.

### <img src="icons/arrow-start.svg" width="18" alt="Start-Arrow Icon"> <img src="icons/arrow-end.svg" width="18" alt="End-Arrow Icon"> Arrow Toggles

These buttons toggle whether
[<img src="icons/segment.svg" width="18" alt="Segment Icon"> Segments](#-segment-tool)
include arrowheads at the start and/or end.

### <img src="icons/highlighter.svg" width="18" alt="Transparent Icon"> Transparent Toggle

This button toggles whether objects are partially transparent/opaque.
Expand Down
7 changes: 6 additions & 1 deletion doc/icons/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Cocreate Documentation Icons

These icons are generated automatically from
Most of these icons are generated automatically from
[icons.coffee](../../client/lib/icons.coffee) via the
[make.coffee](make.coffee) script.

Expand All @@ -9,3 +9,8 @@ To run the script (e.g., when an icon changes or is added):
```sh
npm run doc:icons
```

A few icons are drawn manually:

* `rainbow.svg` based on `#customColor` CSS
* `arrow-start.svg` and `arrow-end.svg` based on `client/tools/arrow.coffee`
8 changes: 8 additions & 0 deletions doc/icons/arrow-end.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions doc/icons/arrow-start.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 0 additions & 2 deletions doc/icons/make.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,3 @@ for name, svg of icons
</svg>
"""

# rainbow.svg made manually based on #customColor CSS
6 changes: 6 additions & 0 deletions lib/objects.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ Meteor.methods
pts: [xyType]
color: String
width: Number
arrowStart: Match.Optional Match.OneOf String, null
arrowEnd: Match.Optional Match.OneOf String, null
when 'rect', 'ellipse'
Object.assign pattern,
pts: Match.Where (pts) ->
Expand Down Expand Up @@ -131,6 +133,10 @@ Meteor.methods
if obj.type in ['pen', 'poly', 'rect', 'ellipse']
Object.assign pattern,
width: Match.Optional Number
if obj.type == 'poly'
Object.assign pattern,
arrowStart: Match.Optional Match.OneOf String, null
arrowEnd: Match.Optional Match.OneOf String, null
if obj.type in ['rect', 'ellipse']
Object.assign pattern,
fill: Match.Optional Match.OneOf String, null # null to turn off
Expand Down

0 comments on commit 7946839

Please sign in to comment.