Skip to content

Commit

Permalink
wip(typescript) keybindings, copy, paste
Browse files Browse the repository at this point in the history
  • Loading branch information
akollegger committed Aug 21, 2023
1 parent 05e17f6 commit 73abfd3
Show file tree
Hide file tree
Showing 11 changed files with 383 additions and 980 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {getPresentGraph} from "../selectors";
import {selectedNodes, selectedRelationships} from "../model/selection";

export const handleCopy = () => {
return function (dispatch, getState) {
return function (dispatch:any, getState: () => any) {
const state = getState()
const graph = getPresentGraph(state)
const nodes = selectedNodes(graph, state.selection)
Expand All @@ -14,9 +14,9 @@ export const handleCopy = () => {
}
const jsonString = JSON.stringify(selectedGraph, null, 2)
navigator.clipboard.writeText(jsonString).then(function() {
console.log("Copied to clipboard successfully!");
console.log("Copied to clipboard successfully! --ABK");
}, function() {
console.error("Unable to write to clipboard. :-(");
console.error("Unable to write to clipboard. :-( --abk");
});
}
}
197 changes: 197 additions & 0 deletions apps/arrows-ts/src/actions/geometricSnapping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import {Point} from "../model-old/Point";
import {idsMatch} from "../model-old/Id";
import {LineGuide} from "../model-old/guides/LineGuide";
import {CircleGuide} from "../model-old/guides/CircleGuide";
import {areParallel} from "../model-old/guides/intersections";
import {byAscendingError} from "../model-old/guides/guides";

export const snapTolerance = 20
const grossTolerance = snapTolerance * 2
export const angleTolerance = Math.PI / 4

export const snapToNeighbourDistancesAndAngles = (graph, snappingNodeId, naturalPosition, otherSelectedNodes) => {

const neighbours = [];
graph.relationships.forEach((relationship) => {
if (idsMatch(relationship.fromId, snappingNodeId) && !otherSelectedNodes.includes(relationship.toId)) {
neighbours.push(graph.nodes.find((node) => idsMatch(node.id, relationship.toId)))
} else if (idsMatch(relationship.toId, snappingNodeId) && !otherSelectedNodes.includes(relationship.fromId)) {
neighbours.push(graph.nodes.find((node) => idsMatch(node.id, relationship.fromId)))
}
})

const includeNode = (nodeId) => !idsMatch(nodeId, snappingNodeId) && !otherSelectedNodes.includes(nodeId)

return snapToDistancesAndAngles(graph, neighbours, includeNode, naturalPosition)
}

export const snapToDistancesAndAngles = (graph, neighbours, includeNode, naturalPosition) => {

const isNeighbour = (nodeId) => !!neighbours.find(neighbour => neighbour.id === nodeId)
let snappedPosition = naturalPosition

let possibleGuides = []

const neighbourRelationships = {};
const collectRelationship = (neighbourNodeId, nonNeighbourNodeId) => {
const pair = {
neighbour: graph.nodes.find((node) => idsMatch(node.id, neighbourNodeId)),
nonNeighbour: graph.nodes.find((node) => idsMatch(node.id, nonNeighbourNodeId))
}
const pairs = neighbourRelationships[pair.neighbour.id] || []
pairs.push(pair)
neighbourRelationships[pair.neighbour.id] = pairs
}

graph.relationships.forEach((relationship) => {
if (isNeighbour(relationship.fromId) && includeNode(relationship.toId)) {
collectRelationship(relationship.fromId, relationship.toId)
}
if (includeNode(relationship.fromId) && isNeighbour(relationship.toId)) {
collectRelationship(relationship.toId, relationship.fromId)
}
})

const snappingAngles = [6, 4, 3]
.map(denominator => Math.PI / denominator)
.flatMap(angle => [-1, -0.5, 0, 0.5].map(offset => offset * Math.PI + angle))

for (const neighbourA of neighbours) {
const relationshipDistances = []

for (const relationship of neighbourRelationships[neighbourA.id] || []) {
const relationshipVector = relationship.nonNeighbour.position.vectorFrom(relationship.neighbour.position);
const distance = relationshipVector.distance()
const similarDistance = relationshipDistances.includes((entry) => Math.abs(entry - distance) < 0.01);
if (!similarDistance) {
relationshipDistances.push(distance)
}

const guide = new LineGuide(neighbourA.position, relationshipVector.angle(), naturalPosition)
if (guide.error < grossTolerance) {
possibleGuides.push(guide)
}
}

for (const distance of relationshipDistances) {
const distanceGuide = new CircleGuide(neighbourA.position, distance, naturalPosition)
if (distanceGuide.error < grossTolerance) {
possibleGuides.push(distanceGuide)
}
}

snappingAngles.forEach(snappingAngle => {
const diagonalGuide = new LineGuide(neighbourA.position, snappingAngle, naturalPosition)
const offset = naturalPosition.vectorFrom(neighbourA.position)
if (diagonalGuide.error < grossTolerance && Math.abs(offset.angle() - snappingAngle) < angleTolerance) {
possibleGuides.push(diagonalGuide)
}
})

for (const neighbourB of neighbours) {
if (neighbourA.id < neighbourB.id) {
const interNeighbourVector = neighbourB.position.vectorFrom(neighbourA.position)
const segment1 = naturalPosition.vectorFrom(neighbourA.position)
const segment2 = neighbourB.position.vectorFrom(naturalPosition)
const parallelGuide = new LineGuide(neighbourA.position, interNeighbourVector.angle(), naturalPosition)
if (parallelGuide.error < grossTolerance && segment1.dot(segment2) > 0) {
possibleGuides.push(parallelGuide)
}

const midPoint = neighbourA.position.translate(interNeighbourVector.scale(0.5))
const perpendicularGuide = new LineGuide(
midPoint,
interNeighbourVector.rotate(Math.PI / 2).angle(),
naturalPosition
)

if (perpendicularGuide.error < grossTolerance) {
possibleGuides.push(perpendicularGuide)
}
}
}
}

const columns = new Set()
const rows = new Set()
graph.nodes.forEach((node) => {
if (includeNode(node.id)) {
if (Math.abs(naturalPosition.x - node.position.x) < grossTolerance) {
columns.add(node.position.x)
}
if (Math.abs(naturalPosition.y - node.position.y) < grossTolerance) {
rows.add(node.position.y)
}
}
})
for (const column of columns) {
possibleGuides.push(new LineGuide(
new Point(column, naturalPosition.y),
Math.PI / 2,
naturalPosition
))
}
for (const row of rows) {
possibleGuides.push(new LineGuide(
new Point(naturalPosition.x, row),
0,
naturalPosition
))
}

const includedNodes = graph.nodes.filter(node => includeNode(node.id))
const intervalGuides = []
for (const guide of possibleGuides) {
const intervalGuide = guide.intervalGuide(includedNodes, naturalPosition)
if (intervalGuide && intervalGuide.error < grossTolerance) {
intervalGuides.push(intervalGuide)
}
}
possibleGuides.push(...intervalGuides)

const candidateGuides = [...possibleGuides]
candidateGuides.sort(byAscendingError)

const guidelines = []

while (guidelines.length === 0 && candidateGuides.length > 0) {
const candidateGuide = candidateGuides.shift()
if (candidateGuide.error < snapTolerance) {
guidelines.push(candidateGuide)
snappedPosition = candidateGuide.snap(naturalPosition)
}
}

while (guidelines.length === 1 && candidateGuides.length > 0) {
const candidateGuide = candidateGuides.shift()
const combination = guidelines[0].combine(candidateGuide, naturalPosition)
if (combination.possible) {
const error = combination.intersection.vectorFrom(naturalPosition).distance()
if (error < snapTolerance) {
guidelines.push(candidateGuide)
snappedPosition = combination.intersection
}
}
}

const lineGuides = guidelines.filter(guide => guide.type === 'LINE')
for (const candidateGuide of possibleGuides) {
if (!guidelines.includes(candidateGuide) &&
candidateGuide.calculateError(snappedPosition) < 0.01) {
if (candidateGuide.type === 'LINE') {
if (lineGuides.every(guide => !areParallel(guide, candidateGuide))) {
lineGuides.push(candidateGuide)
guidelines.push(candidateGuide)
}
} else {
guidelines.push(candidateGuide)
}
}
}

return {
snapped: guidelines.length > 0,
guidelines,
snappedPosition
}
}
4 changes: 3 additions & 1 deletion apps/arrows-ts/src/actions/import.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@ export const interpretClipboardData = (clipboardData, nodeSpacing, handlers) =>
try {
switch (format.outputType) {
case 'graph':
// eslint-disable-next-line no-case-declarations
const importedGraph = format.parse(text, nodeSpacing)
handlers.onGraph && handlers.onGraph(importedGraph)
break

case 'svg':
// eslint-disable-next-line no-case-declarations
const svgImageUrl = format.parse(text)
handlers.onSvgImageUrl && handlers.onSvgImageUrl(svgImageUrl)
break
Expand Down Expand Up @@ -101,7 +103,7 @@ export const handlePaste = (pasteEvent) => {
const formats = [
{
// JSON
recognise: (plainText) => new RegExp('^{.*\}$', 's').test(plainText.trim()),
recognise: (plainText) => new RegExp('^{.*}$', 's').test(plainText.trim()),
outputType: 'graph',
parse: (plainText) => {
const object = JSON.parse(plainText)
Expand Down
53 changes: 36 additions & 17 deletions apps/arrows-ts/src/app/App.jsx → apps/arrows-ts/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ import GraphContainer from "../containers/GraphContainer"
import {connect} from 'react-redux'
import withKeybindings, { ignoreTarget } from '../interactions/Keybindings'
import {windowResized} from "../actions/applicationLayout"
import { compose } from 'react-recompose'
import HeaderContainer from '../containers/HeaderContainer'
import InspectorChooser from "../containers/InspectorChooser"
import {computeCanvasSize, inspectorWidth} from "../model/applicationLayout";
import {computeCanvasSize, inspectorWidth} from "@neo4j-arrows/model";
import ExportContainer from "../containers/ExportContainer";
import GoogleSignInModal from "../components/editors/GoogleSignInModal";
import HelpModal from "../components/HelpModal";
Expand All @@ -23,8 +22,25 @@ import {handleImportMessage} from "../reducers/storage";

import './App.css'

class App extends Component {
constructor (props) {
export interface AppProps {
inspectorVisible:boolean;
showSaveAsDialog:boolean;
showExportDialog:boolean;
showImportDialog:boolean;
pickingFromGoogleDrive:boolean;
pickingFromLocalStorage:boolean;
onCancelPicker:any;
loadFromGoogleDrive:any;
canvasHeight:number;
fireAction:any;
handleCopy: (ev:ClipboardEvent) => void;
handlePaste: (ev:ClipboardEvent) => void;
handleImportMessage: (ev:MessageEvent<any>) => void;
onWindowResized: (this: Window, ev: UIEvent) => any
}

class App extends Component<AppProps> {
constructor (props:AppProps) {
super(props)
linkToGoogleFontsCss()
window.addEventListener('keydown', this.fireKeyboardShortcutAction.bind(this))
Expand Down Expand Up @@ -94,7 +110,7 @@ class App extends Component {
);
}

fireKeyboardShortcutAction(ev) {
fireKeyboardShortcutAction(ev:KeyboardEvent) {
if (ignoreTarget(ev)) return

const handled = this.props.fireAction(ev)
Expand All @@ -104,17 +120,18 @@ class App extends Component {
}
}

handleCopy(ev) {
handleCopy(ev:ClipboardEvent) {
if (ignoreTarget(ev)) return
console.log('copying')
this.props.handleCopy(ev)
}

handlePaste(ev) {
handlePaste(ev:ClipboardEvent) {
if (ignoreTarget(ev)) return
this.props.handlePaste(ev)
}

handleMessage(ev) {
handleMessage(ev:MessageEvent<any>) {
this.props.handleImportMessage(ev)
}

Expand All @@ -123,7 +140,7 @@ class App extends Component {
}
}

const mapStateToProps = (state) => ({
const mapStateToProps = (state:any) => ({
inspectorVisible: state.applicationLayout.inspectorVisible,
canvasHeight: computeCanvasSize(state.applicationLayout).height,
pickingFromGoogleDrive: state.storage.status === 'PICKING_FROM_GOOGLE_DRIVE',
Expand All @@ -134,18 +151,20 @@ const mapStateToProps = (state) => ({
})


const mapDispatchToProps = dispatch => {
const mapDispatchToProps = (dispatch:any) => {
return {
onWindowResized: () => dispatch(windowResized(window.innerWidth, window.innerHeight)),
onCancelPicker: () => dispatch(pickDiagramCancel()),
loadFromGoogleDrive: fileId => dispatch(getFileFromGoogleDrive(fileId)),
loadFromGoogleDrive: (fileId:any) => dispatch(getFileFromGoogleDrive(fileId)),
handleCopy: () => dispatch(handleCopy()),
handlePaste: clipboardEvent => dispatch(handlePaste(clipboardEvent)),
handleImportMessage: message => dispatch(handleImportMessage(message))
handlePaste: (clipboardEvent:any) => dispatch(handlePaste(clipboardEvent)),
handleImportMessage: (message:any) => dispatch(handleImportMessage(message))
}
}

export default compose(
connect(mapStateToProps, mapDispatchToProps),
withKeybindings
)(App)
// NOTE: compose(a,b,c)(X) ==[BECOMES]=> a(b(c(X)))
// export default compose(
// connect(mapStateToProps, mapDispatchToProps),
// withKeybindings
// )(App)
export default connect(mapStateToProps, mapDispatchToProps)(withKeybindings(App))
1 change: 0 additions & 1 deletion apps/arrows-ts/src/app/app.module.css

This file was deleted.

14 changes: 0 additions & 14 deletions apps/arrows-ts/src/app/app.notsx

This file was deleted.

Loading

0 comments on commit 73abfd3

Please sign in to comment.