Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 28 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rete-history-plugin",
"version": "2.1.1",
"version": "2.2.0",
"description": "",
"scripts": {
"build": "rete build -c rete.config.ts",
Expand All @@ -25,13 +25,20 @@
},
"peerDependencies": {
"rete": "^2.0.1",
"rete-area-plugin": "^2.0.0"
"rete-area-plugin": "^2.0.0",
"rete-comment-plugin": "^2.2.0"
},
"peerDependenciesMeta": {
"rete-comment-plugin": {
"optional": true
}
},
"devDependencies": {
"globals": "^15.9.0",
"rete": "^2.0.1",
"rete-area-plugin": "^2.0.0",
"rete-cli": "~2.0.1",
"rete-comment-plugin": "^2.2.0",
"typescript": "~4.8.4"
},
"dependencies": {
Expand Down
3 changes: 2 additions & 1 deletion rete.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default <ReteOptions>{
name: 'ReteHistoryPlugin',
globals: {
'rete': 'Rete',
'rete-area-plugin': 'ReteAreaPlugin'
'rete-area-plugin': 'ReteAreaPlugin',
'rete-comment-plugin': 'ReteCommentPlugin'
}
}
16 changes: 16 additions & 0 deletions src/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,22 @@ export default class History<A extends Action> {
return list
}

removeRecent(predicate: (record: HistoryRecord<A>) => boolean, ms: number) {
const treshold = Date.now() - ms

for (let i = this.produced.length - 1; i >= 0; i--) {
const record = this.produced[i]

if (record.time <= treshold) break
if (record.separated) break

if (predicate(record)) {
this.produced.splice(i, 1)
return record
}
}
}

async move(from: HistoryRecord<A>[], to: HistoryRecord<A>[], type: 'undo' | 'redo') {
const record = from.pop()

Expand Down
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
export * as HistoryExtensions from './extensions'
export * as Presets from './presets'
export type { HistoryActions } from './presets/classic'
export type { CommentHistoryActions } from './presets/comments'
export type { Preset } from './presets/types'

/**
* History plugin. Allows to undo/redo actions such as adding/removing nodes, connections, etc.
*/
export class HistoryPlugin<Schemes extends BaseSchemes, A extends Action = Action> extends Scope<never, [BaseArea<Schemes>, Root<Schemes>]> {

Check warning on line 18 in src/index.ts

View workflow job for this annotation

GitHub Actions / ci / ci

This line has a length of 141. Maximum allowed is 120

Check warning on line 18 in src/index.ts

View workflow job for this annotation

GitHub Actions / ci / ci

This line has a length of 141. Maximum allowed is 120
private history = new History<A>({})
private editor!: NodeEditor<Schemes>
private area!: BaseAreaPlugin<Schemes, BaseArea<Schemes>>
Expand Down Expand Up @@ -56,8 +57,8 @@
* @param preset Preset that manages some actions, e.g. classic preset for adding/removing nodes, connections, etc.
*/
public addPreset<T extends A>(preset: Preset<Schemes, T>) {
this.presets.push(preset as unknown as Preset<Schemes, A>)

Check warning on line 60 in src/index.ts

View workflow job for this annotation

GitHub Actions / ci / ci

The 'preset as unknown as Preset<Schemes, A>' has unsafe 'as' type assertion

Check warning on line 60 in src/index.ts

View workflow job for this annotation

GitHub Actions / ci / ci

The 'preset as unknown as Preset<Schemes, A>' has unsafe 'as' type assertion
if (this.area && this.editor) (preset as unknown as Preset<Schemes, A>).connect(this)

Check warning on line 61 in src/index.ts

View workflow job for this annotation

GitHub Actions / ci / ci

The 'preset as unknown as Preset<Schemes, A>' has unsafe 'as' type assertion

Check warning on line 61 in src/index.ts

View workflow job for this annotation

GitHub Actions / ci / ci

Unnecessary conditional, value is always truthy

Check warning on line 61 in src/index.ts

View workflow job for this annotation

GitHub Actions / ci / ci

Unnecessary conditional, value is always truthy

Check warning on line 61 in src/index.ts

View workflow job for this annotation

GitHub Actions / ci / ci

The 'preset as unknown as Preset<Schemes, A>' has unsafe 'as' type assertion

Check warning on line 61 in src/index.ts

View workflow job for this annotation

GitHub Actions / ci / ci

Unnecessary conditional, value is always truthy

Check warning on line 61 in src/index.ts

View workflow job for this annotation

GitHub Actions / ci / ci

Unnecessary conditional, value is always truthy
}

/**
Expand Down Expand Up @@ -85,6 +86,13 @@
return this.history.getRecent(ms)
}

/**
* Removes the most recent history record matching the predicate within the time window
*/
public removeRecent(predicate: (record: { time: number, action: A }) => boolean, ms?: number) {
return this.history.removeRecent(predicate, ms ?? this.timing * 2)
}

/**
* Clear history
*/
Expand All @@ -98,7 +106,7 @@
public separate() {
const latest = this.history.produced[this.history.produced.length - 1]

if (latest) latest.separated = true

Check warning on line 109 in src/index.ts

View workflow job for this annotation

GitHub Actions / ci / ci

Unnecessary conditional, value is always truthy

Check warning on line 109 in src/index.ts

View workflow job for this annotation

GitHub Actions / ci / ci

Unnecessary conditional, value is always truthy
}

/**
Expand All @@ -110,7 +118,7 @@
if (record) {
const latest = this.history.produced[this.history.produced.length - 1]

if (latest && !latest.separated && latest.time + this.timing > record.time) {

Check warning on line 121 in src/index.ts

View workflow job for this annotation

GitHub Actions / ci / ci

Unnecessary conditional, value is always truthy

Check warning on line 121 in src/index.ts

View workflow job for this annotation

GitHub Actions / ci / ci

Unnecessary conditional, value is always truthy
await this.undo()
}
}
Expand All @@ -125,7 +133,7 @@
if (record) {
const latest = this.history.reserved[this.history.reserved.length - 1]

if (latest && !record.separated && record.time + this.timing > latest.time) {

Check warning on line 136 in src/index.ts

View workflow job for this annotation

GitHub Actions / ci / ci

Unnecessary conditional, value is always truthy

Check warning on line 136 in src/index.ts

View workflow job for this annotation

GitHub Actions / ci / ci

Unnecessary conditional, value is always truthy
await this.redo()
}
}
Expand Down
221 changes: 221 additions & 0 deletions src/presets/comments/actions/comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import { BaseSchemes, NodeId } from 'rete'
import { BaseAreaPlugin } from 'rete-area-plugin'
import {
Comment,
CommentPlugin,
CommentRestoreData,
FrameBounds,
FrameComment
} from 'rete-comment-plugin'

import { Action } from '../../../types'

export type CommentSnapshot = CommentRestoreData

export type FrameState = FrameBounds & { links: string[] }

type Position = { x: number, y: number }

type LinkedNodeSnapshot = { id: string, prev: Position, new: Position }

export function commentSnapshot(comment: Comment): CommentSnapshot {
return {
id: comment.id,
text: comment.text,
x: comment.x,
y: comment.y,
width: comment.width,
height: comment.height,
links: [...comment.links],
kind: comment instanceof FrameComment
? 'frame'
: 'inline'
}
}

export class AddCommentAction implements Action {
private stored?: CommentSnapshot

constructor(
private comment: CommentPlugin<any, any>,
private commentId: string
) { }

undo() {
const item = this.comment.comments.get(this.commentId)

if (!item) return

this.stored = commentSnapshot(item)
this.comment.delete(this.commentId)
}

redo() {
if (!this.stored) return

this.comment.restoreComment(this.stored)
}
}

export class RemoveCommentAction implements Action {
constructor(
private comment: CommentPlugin<any, any>,
private stored: CommentSnapshot
) { }

undo() {
this.comment.restoreComment(this.stored)
}

redo() {
this.comment.delete(this.stored.id)
}
}

export class DragCommentAction implements Action {
prev: Position
new: Position
prevLinks: string[]
newLinks: string[]
private prevFrame?: FrameState
private newFrame?: FrameState
private nodes: LinkedNodeSnapshot[]

constructor(
private comment: CommentPlugin<any, any>,
private area: BaseAreaPlugin<BaseSchemes, any>,
public commentId: string,
prev: Position,
current: Position,
prevLinks: string[],
newLinks: string[],
nodes?: { id: string, previous: Position, current: Position }[]
) {
this.prev = { ...prev }
this.new = { ...current }
this.prevLinks = [...prevLinks]
this.newLinks = [...newLinks]
this.nodes = (nodes ?? []).map(({ id, previous, current: next }) => ({
id,
prev: { ...previous },
new: { ...next }
}))

const item = comment.comments.get(commentId)

// Frame drag changes position only; bounds stay until membership/resize elsewhere
if (item instanceof FrameComment) {
this.prevFrame = {
x: prev.x,
y: prev.y,
width: item.width,
height: item.height,
links: [...prevLinks]
}
this.newFrame = {
x: item.x,
y: item.y,
width: item.width,
height: item.height,
links: [...newLinks]
}
}
}

private async applyInline(position: Position, links: string[]) {
const item = this.comment.comments.get(this.commentId)

if (!item) return

await this.comment.translate(this.commentId, position.x - item.x, position.y - item.y)
await this.comment.setCommentLinks(this.commentId, links)
}

private async applyFrame(frame: FrameState, nodeTarget: 'prev' | 'new') {
this.comment.setFrameState(this.commentId, frame)

for (const node of this.nodes) {
const position = nodeTarget === 'prev'
? node.prev
: node.new
const view = this.area.nodeViews.get(node.id)

if (view) await view.translate(position.x, position.y)
}
}

async undo() {
if (this.prevFrame) {
await this.applyFrame(this.prevFrame, 'prev')
return
}

await this.applyInline(this.prev, this.prevLinks)
}

async redo() {
if (this.newFrame) {
await this.applyFrame(this.newFrame, 'new')
return
}

await this.applyInline(this.new, this.newLinks)
}
}

export class NodeFrameMembershipAction implements Action {
constructor(
private comment: CommentPlugin<any, any>,
private frames: {
id: string
prev: FrameState
next: FrameState
}[],
private node: { id: NodeId, prev: Position, new: Position } | null
) { }

async undo() {
await this.comment.restoreNodeFrameMembership({
frames: this.frames.map(frame => ({ id: frame.id, state: frame.prev })),
...this.node
? { node: { id: this.node.id, position: this.node.prev } }
: {}
})
}

async redo() {
await this.comment.restoreNodeFrameMembership({
frames: this.frames.map(frame => ({ id: frame.id, state: frame.next })),
...this.node
? { node: { id: this.node.id, position: this.node.new } }
: {}
})
}
}

export class EditCommentAction implements Action {
constructor(
private comment: CommentPlugin<any, any>,
private commentId: string,
private previousText: string,
private newText: string
) { }

undo() {
const item = this.comment.comments.get(this.commentId)

if (!item) return

item.text = this.previousText
item.update()
}

redo() {
const item = this.comment.comments.get(this.commentId)

if (!item) return

item.text = this.newText
item.update()
}
}
Loading
Loading