Compare commits
5 Commits
629274476c
...
7388a15cff
Author | SHA1 | Date |
---|---|---|
Ladd Hoffman | 7388a15cff | |
Ladd Hoffman | 4026d7eaa8 | |
Ladd Hoffman | 2195f5ea56 | |
Ladd Hoffman | ec3401845d | |
Ladd Hoffman | 435633a893 |
|
@ -0,0 +1,17 @@
|
||||||
|
Reputation is comprised of non-fungible tokens associated with specific forum graph post -> author edges.
|
||||||
|
Therefore in principle, all information about the context of a given rep token can be derived by inspecting the forum graph.
|
||||||
|
However, in practice, the computational costs and the difficulty of preserving complete records will increase over time.
|
||||||
|
It is for this reason that we compute the current value of a given rep token and store that value.
|
||||||
|
Although the value could be recomputed when needed, it would be (unpredictably) expensive and time-consuming to do so.
|
||||||
|
|
||||||
|
In its current, singular form, all instances of reputation within a given DAO have equal power, assuming equal numeric value.
|
||||||
|
|
||||||
|
However, the question arises: what would it take to support the ability to initiate a validation pool in which the power of a reputation token
|
||||||
|
depends on something more than just its numeric value?
|
||||||
|
|
||||||
|
This would be something specified when a validation pool is initiated.
|
||||||
|
|
||||||
|
Suppose we support the notion of distinct types of reputation within a given DAO.
|
||||||
|
Let's say we have reputation type A and B.
|
||||||
|
Let's say we have a validation pool that requires reputation type A to vote, and mints reputation type A.
|
||||||
|
That means governance is separated.
|
|
@ -25,7 +25,7 @@ export class Expert extends ReputationHolder {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
const authorEdges = authorVertex.getEdges(EdgeTypes.AUTHOR, false);
|
const authorEdges = authorVertex.getEdges(EdgeTypes.AUTHOR, false);
|
||||||
const tokenValues = authorEdges.map(({ data: { tokenId } }) => this.dao.reputation.valueOf(tokenId));
|
const tokenValues = authorEdges.map(({ data: { tokenAddress } }) => this.dao.reputation.valueOf(tokenAddress));
|
||||||
return tokenValues.reduce((value, total) => total += value, 0);
|
return tokenValues.reduce((value, total) => total += value, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ export class Expert extends ReputationHolder {
|
||||||
await this.actions.submitPost.log(this, post);
|
await this.actions.submitPost.log(this, post);
|
||||||
const postId = post.id;
|
const postId = post.id;
|
||||||
const pool = await this.initiateValidationPool({ fee, postId }, params);
|
const pool = await this.initiateValidationPool({ fee, postId }, params);
|
||||||
this.tokens.push(pool.tokenId);
|
this.tokens.push(pool.tokenAddress);
|
||||||
return { postId, pool };
|
return { postId, pool };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ export class Expert extends ReputationHolder {
|
||||||
postId,
|
postId,
|
||||||
fee,
|
fee,
|
||||||
}, params);
|
}, params);
|
||||||
this.tokens.push(pool.tokenId);
|
this.tokens.push(pool.tokenAddress);
|
||||||
return pool;
|
return pool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ export class Expert extends ReputationHolder {
|
||||||
`(${position ? 'for' : 'against'}, stake: ${amount})`,
|
`(${position ? 'for' : 'against'}, stake: ${amount})`,
|
||||||
);
|
);
|
||||||
return validationPool.stake(this.reputationPublicKey, {
|
return validationPool.stake(this.reputationPublicKey, {
|
||||||
position, amount, lockingTime, tokenId: this.tokens[0],
|
position, amount, lockingTime, tokenAddress: this.tokens[0],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ export class Expert extends ReputationHolder {
|
||||||
);
|
);
|
||||||
this.workerId = await this.dao.availability.register(this.reputationPublicKey, {
|
this.workerId = await this.dao.availability.register(this.reputationPublicKey, {
|
||||||
stakeAmount,
|
stakeAmount,
|
||||||
tokenId: this.tokens[0],
|
tokenAddress: this.tokens[0],
|
||||||
duration,
|
duration,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,9 @@ import { Actor } from '../display/actor.js';
|
||||||
import { CryptoUtil } from '../supporting/crypto.js';
|
import { CryptoUtil } from '../supporting/crypto.js';
|
||||||
|
|
||||||
class Worker {
|
class Worker {
|
||||||
constructor(reputationPublicKey, tokenId, stakeAmount, duration) {
|
constructor(reputationPublicKey, tokenAddress, stakeAmount, duration) {
|
||||||
this.reputationPublicKey = reputationPublicKey;
|
this.reputationPublicKey = reputationPublicKey;
|
||||||
this.tokenId = tokenId;
|
this.tokenAddress = tokenAddress;
|
||||||
this.stakeAmount = stakeAmount;
|
this.stakeAmount = stakeAmount;
|
||||||
this.duration = duration;
|
this.duration = duration;
|
||||||
this.available = true;
|
this.available = true;
|
||||||
|
@ -28,11 +28,11 @@ export class Availability extends Actor {
|
||||||
this.workers = new Map();
|
this.workers = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
register(reputationPublicKey, { stakeAmount, tokenId, duration }) {
|
register(reputationPublicKey, { stakeAmount, tokenAddress, duration }) {
|
||||||
// TODO: Should be signed by token owner
|
// TODO: Should be signed by token owner
|
||||||
this.dao.reputation.lock(tokenId, stakeAmount, duration);
|
this.dao.reputation.lock(tokenAddress, stakeAmount, duration);
|
||||||
const workerId = CryptoUtil.randomUUID();
|
const workerId = CryptoUtil.randomUUID();
|
||||||
this.workers.set(workerId, new Worker(reputationPublicKey, tokenId, stakeAmount, duration));
|
this.workers.set(workerId, new Worker(reputationPublicKey, tokenAddress, stakeAmount, duration));
|
||||||
return workerId;
|
return workerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -87,7 +87,7 @@ export class Business extends Actor {
|
||||||
});
|
});
|
||||||
|
|
||||||
await pool.stake(reputationPublicKey, {
|
await pool.stake(reputationPublicKey, {
|
||||||
tokenId: request.worker.tokenId,
|
tokenAddress: request.worker.tokenAddress,
|
||||||
amount: request.worker.stakeAmount,
|
amount: request.worker.stakeAmount,
|
||||||
position: true,
|
position: true,
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { Availability } from './availability.js';
|
||||||
import { Business } from './business.js';
|
import { Business } from './business.js';
|
||||||
import { Voter } from '../supporting/voter.js';
|
import { Voter } from '../supporting/voter.js';
|
||||||
import { Actor } from '../display/actor.js';
|
import { Actor } from '../display/actor.js';
|
||||||
|
import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../util/constants.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Purpose:
|
* Purpose:
|
||||||
|
@ -49,15 +50,13 @@ export class DAO extends Actor {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getActiveReputation() {
|
/**
|
||||||
|
* @param {number} param0.reputationTypeId
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
getActiveAvailableReputation({ reputationTypeId = DEFAULT_REP_TOKEN_TYPE_ID }) {
|
||||||
return this.listActiveVoters()
|
return this.listActiveVoters()
|
||||||
.map(({ reputationPublicKey }) => this.reputation.valueOwnedBy(reputationPublicKey))
|
.map(({ reputationPublicKey }) => this.reputation.availableValueOwnedBy(reputationPublicKey, reputationTypeId))
|
||||||
.reduce((acc, cur) => (acc += cur), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
getActiveAvailableReputation() {
|
|
||||||
return this.listActiveVoters()
|
|
||||||
.map(({ reputationPublicKey }) => this.reputation.availableValueOwnedBy(reputationPublicKey))
|
|
||||||
.reduce((acc, cur) => (acc += cur), 0);
|
.reduce((acc, cur) => (acc += cur), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { WeightedDirectedGraph } from '../supporting/wdg.js';
|
|
||||||
import { Action } from '../display/action.js';
|
import { Action } from '../display/action.js';
|
||||||
import { Actor } from '../display/actor.js';
|
import { Actor } from '../display/actor.js';
|
||||||
import { ReputationHolder } from '../reputation/reputation-holder.js';
|
import { ReputationHolder } from '../reputation/reputation-holder.js';
|
||||||
|
@ -6,6 +5,7 @@ import { displayNumber } from '../../util/helpers.js';
|
||||||
import {
|
import {
|
||||||
EPSILON, INCINERATOR_ADDRESS, EdgeTypes, VertexTypes,
|
EPSILON, INCINERATOR_ADDRESS, EdgeTypes, VertexTypes,
|
||||||
} from '../../util/constants.js';
|
} from '../../util/constants.js';
|
||||||
|
import { WDGDiagram } from '../display/wdg-mermaid-ui.js';
|
||||||
|
|
||||||
class Post extends Actor {
|
class Post extends Actor {
|
||||||
constructor(forum, senderId, postContent) {
|
constructor(forum, senderId, postContent) {
|
||||||
|
@ -15,8 +15,8 @@ class Post extends Actor {
|
||||||
this.forum = forum;
|
this.forum = forum;
|
||||||
this.id = postContent.id ?? name;
|
this.id = postContent.id ?? name;
|
||||||
this.senderId = senderId;
|
this.senderId = senderId;
|
||||||
this.value = 0;
|
this.values = new Map();
|
||||||
this.initialValue = 0;
|
this.initialValues = new Map();
|
||||||
this.authors = postContent.authors;
|
this.authors = postContent.authors;
|
||||||
this.citations = postContent.citations;
|
this.citations = postContent.citations;
|
||||||
this.title = postContent.title;
|
this.title = postContent.title;
|
||||||
|
@ -44,7 +44,7 @@ export class Forum extends ReputationHolder {
|
||||||
super(name, scene);
|
super(name, scene);
|
||||||
this.dao = dao;
|
this.dao = dao;
|
||||||
this.id = this.reputationPublicKey;
|
this.id = this.reputationPublicKey;
|
||||||
this.graph = new WeightedDirectedGraph('forum', scene);
|
this.graph = new WDGDiagram('forum', scene);
|
||||||
this.actions = {
|
this.actions = {
|
||||||
propagate: new Action('propagate', scene),
|
propagate: new Action('propagate', scene),
|
||||||
confirm: new Action('confirm', scene),
|
confirm: new Action('confirm', scene),
|
||||||
|
@ -83,16 +83,21 @@ export class Forum extends ReputationHolder {
|
||||||
// getContract(type) { }
|
// getContract(type) { }
|
||||||
|
|
||||||
async onValidate({
|
async onValidate({
|
||||||
pool, postId, tokenId, referenceChainLimit, leachingValue,
|
pool, postId, tokenAddress, referenceChainLimit, leachingValue,
|
||||||
}) {
|
}) {
|
||||||
console.log('onValidate', { pool, postId, tokenId });
|
console.log('onValidate', { pool, postId, tokenAddress });
|
||||||
const initialValue = this.dao.reputation.valueOf(tokenId);
|
|
||||||
|
// What we have here now is an ERC-1155 rep token, which can contain multiple reputation types.
|
||||||
|
// TODO: ERC-1155 supports a batch transfer operation, so it probably makes sense to leverage that.
|
||||||
|
|
||||||
|
const initialValues = new Map(pool.reputationTypeIds
|
||||||
|
.map((tokenTypeId) => [tokenTypeId, this.dao.reputation.valueOf(tokenAddress, tokenTypeId)]));
|
||||||
const postVertex = this.graph.getVertex(postId);
|
const postVertex = this.graph.getVertex(postId);
|
||||||
const post = postVertex.data;
|
const post = postVertex.data;
|
||||||
post.setStatus('Validated');
|
post.setStatus('Validated');
|
||||||
post.initialValue = initialValue;
|
post.initialValues = initialValues;
|
||||||
|
|
||||||
const addAuthorToGraph = (publicKey, weight, authorTokenId) => {
|
const addAuthorToGraph = (publicKey, weight, authorTokenAddress) => {
|
||||||
// For graph display purposes, we want to use the existing Expert actors from the current scene.
|
// For graph display purposes, we want to use the existing Expert actors from the current scene.
|
||||||
const author = this.scene.findActor(({ reputationPublicKey }) => reputationPublicKey === publicKey);
|
const author = this.scene.findActor(({ reputationPublicKey }) => reputationPublicKey === publicKey);
|
||||||
author.setDisplayValue('reputation', () => author.getReputation());
|
author.setDisplayValue('reputation', () => author.getReputation());
|
||||||
|
@ -105,7 +110,7 @@ export class Forum extends ReputationHolder {
|
||||||
postVertex,
|
postVertex,
|
||||||
authorVertex,
|
authorVertex,
|
||||||
weight,
|
weight,
|
||||||
{ tokenId: authorTokenId },
|
{ tokenAddress: authorTokenAddress },
|
||||||
{ hide: author.options.hide },
|
{ hide: author.options.hide },
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -114,16 +119,18 @@ export class Forum extends ReputationHolder {
|
||||||
// If no authors are specified, treat the sender as the sole author.
|
// If no authors are specified, treat the sender as the sole author.
|
||||||
// TODO: Verify that cumulative author weight == 1.
|
// TODO: Verify that cumulative author weight == 1.
|
||||||
if (!post.authors?.length) {
|
if (!post.authors?.length) {
|
||||||
addAuthorToGraph(post.senderId, 1, tokenId);
|
addAuthorToGraph(post.senderId, 1, tokenAddress);
|
||||||
} else {
|
} else {
|
||||||
for (const { publicKey, weight } of post.authors) {
|
for (const { publicKey, weight } of post.authors) {
|
||||||
// If the sender is also listed among the authors, do not mint them an additional token.
|
// If the sender is also listed among the authors, do not mint them an additional token.
|
||||||
const authorTokenId = (publicKey === post.senderId) ? tokenId : this.dao.reputation.mint(this.id, 0);
|
const authorTokenAddress = (publicKey === post.senderId)
|
||||||
addAuthorToGraph(publicKey, weight, authorTokenId);
|
? tokenAddress
|
||||||
|
: this.dao.reputation.mintBatch(this.id, pool.reputationTypeIds, pool.reputationTypeIds.map(() => 0));
|
||||||
|
addAuthorToGraph(publicKey, weight, authorTokenAddress);
|
||||||
}
|
}
|
||||||
// If the sender is not an author, they will end up with the minted token but with zero value.
|
// If the sender is not an author, they will end up with the minted token but with zero value.
|
||||||
if (!post.authors.find(({ publicKey }) => publicKey === post.senderId)) {
|
if (!post.authors.find(({ publicKey }) => publicKey === post.senderId)) {
|
||||||
addAuthorToGraph(post.senderId, 0, tokenId);
|
addAuthorToGraph(post.senderId, 0, tokenAddress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,7 +141,7 @@ export class Forum extends ReputationHolder {
|
||||||
{ to: postVertex, from: { data: pool } },
|
{ to: postVertex, from: { data: pool } },
|
||||||
{
|
{
|
||||||
rewardsAccumulator,
|
rewardsAccumulator,
|
||||||
increment: initialValue,
|
increments: initialValues,
|
||||||
referenceChainLimit,
|
referenceChainLimit,
|
||||||
leachingValue,
|
leachingValue,
|
||||||
},
|
},
|
||||||
|
@ -142,16 +149,16 @@ export class Forum extends ReputationHolder {
|
||||||
|
|
||||||
// Apply computed rewards to update values of tokens
|
// Apply computed rewards to update values of tokens
|
||||||
for (const [authorEdge, amount] of rewardsAccumulator) {
|
for (const [authorEdge, amount] of rewardsAccumulator) {
|
||||||
const { to: authorVertex, data: { tokenId: authorTokenId } } = authorEdge;
|
const { to: authorVertex, data: { tokenAddress: authorTokenAddress } } = authorEdge;
|
||||||
const { data: author } = authorVertex;
|
const { data: author } = authorVertex;
|
||||||
// The primary author gets the validation pool minted token.
|
// The primary author gets the validation pool minted token.
|
||||||
// So we don't need to transfer any reputation to the primary author.
|
// So we don't need to transfer any reputation to the primary author.
|
||||||
// Their reward will be the remaining balance after all other transfers.
|
// Their reward will be the remaining balance after all other transfers.
|
||||||
if (authorTokenId !== tokenId) {
|
if (authorTokenAddress !== tokenAddress) {
|
||||||
if (amount < 0) {
|
if (amount < 0) {
|
||||||
this.dao.reputation.transferValueFrom(authorTokenId, tokenId, -amount);
|
this.dao.reputation.transferValueFrom(authorTokenAddress, tokenAddress, -amount);
|
||||||
} else {
|
} else {
|
||||||
this.dao.reputation.transferValueFrom(tokenId, authorTokenId, amount);
|
this.dao.reputation.transferValueFrom(tokenAddress, authorTokenAddress, amount);
|
||||||
}
|
}
|
||||||
await author.computeDisplayValues((label, value) => authorVertex.setProperty(label, value));
|
await author.computeDisplayValues((label, value) => authorVertex.setProperty(label, value));
|
||||||
authorVertex.displayVertex();
|
authorVertex.displayVertex();
|
||||||
|
@ -167,8 +174,8 @@ export class Forum extends ReputationHolder {
|
||||||
for (const authorEdge of postVertex.getEdges(EdgeTypes.AUTHOR, true)) {
|
for (const authorEdge of postVertex.getEdges(EdgeTypes.AUTHOR, true)) {
|
||||||
const authorVertex = authorEdge.to;
|
const authorVertex = authorEdge.to;
|
||||||
const author = authorVertex.data;
|
const author = authorVertex.data;
|
||||||
const { tokenId: authorTokenId } = authorEdge.data;
|
const { tokenAddress: authorTokenAddress } = authorEdge.data;
|
||||||
this.dao.reputation.transfer(this.id, author.reputationPublicKey, authorTokenId);
|
this.dao.reputation.transfer(this.id, author.reputationPublicKey, authorTokenAddress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,7 +185,7 @@ export class Forum extends ReputationHolder {
|
||||||
*/
|
*/
|
||||||
async propagateValue(edge, {
|
async propagateValue(edge, {
|
||||||
rewardsAccumulator,
|
rewardsAccumulator,
|
||||||
increment,
|
increments,
|
||||||
depth = 0,
|
depth = 0,
|
||||||
initialNegative = false,
|
initialNegative = false,
|
||||||
referenceChainLimit,
|
referenceChainLimit,
|
||||||
|
@ -186,7 +193,9 @@ export class Forum extends ReputationHolder {
|
||||||
}) {
|
}) {
|
||||||
const postVertex = edge.to;
|
const postVertex = edge.to;
|
||||||
const post = postVertex.data;
|
const post = postVertex.data;
|
||||||
this.actions.propagate.log(edge.from.data, post, `(${increment})`);
|
const incrementsStrArray = Array.from(increments.entries()).map(([k, v]) => `${k} ${v}`);
|
||||||
|
const incrementsStr = `(${incrementsStrArray.join(')(')})`;
|
||||||
|
this.actions.propagate.log(edge.from.data, post, incrementsStr);
|
||||||
|
|
||||||
if (!!referenceChainLimit && depth > referenceChainLimit) {
|
if (!!referenceChainLimit && depth > referenceChainLimit) {
|
||||||
this.actions.propagate.log(
|
this.actions.propagate.log(
|
||||||
|
@ -196,105 +205,114 @@ export class Forum extends ReputationHolder {
|
||||||
null,
|
null,
|
||||||
'-x',
|
'-x',
|
||||||
);
|
);
|
||||||
return increment;
|
return increments;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('propagateValue start', {
|
console.log('propagateValue start', {
|
||||||
from: edge.from.id ?? edge.from,
|
from: edge.from.id ?? edge.from,
|
||||||
to: edge.to.id,
|
to: edge.to.id,
|
||||||
depth,
|
depth,
|
||||||
value: post.value,
|
values: post.values,
|
||||||
increment,
|
increments,
|
||||||
initialNegative,
|
initialNegative,
|
||||||
});
|
});
|
||||||
|
|
||||||
const propagate = async (positive) => {
|
const propagate = async (positive) => {
|
||||||
let totalOutboundAmount = 0;
|
const totalOutboundAmounts = increments.map(() => 0);
|
||||||
const citationEdges = postVertex.getEdges(EdgeTypes.CITATION, true)
|
const citationEdges = postVertex.getEdges(EdgeTypes.CITATION, true)
|
||||||
.filter(({ weight }) => (positive ? weight > 0 : weight < 0));
|
.filter(({ weight }) => (positive ? weight > 0 : weight < 0));
|
||||||
for (const citationEdge of citationEdges) {
|
for (const citationEdge of citationEdges) {
|
||||||
const { weight } = citationEdge;
|
const { weight } = citationEdge;
|
||||||
let outboundAmount = weight * increment;
|
const outboundAmounts = increments.map((increment) => weight * increment);
|
||||||
if (Math.abs(outboundAmount) > EPSILON) {
|
const refundsFromOutbound = increments.map(() => 0);
|
||||||
const balanceToOutbound = this.graph.getEdgeWeight(EdgeTypes.BALANCE, citationEdge.from, citationEdge.to)
|
for (let idx = 0; idx < outboundAmounts.length; idx++) {
|
||||||
|
let outboundAmount = outboundAmounts[idx];
|
||||||
|
if (Math.abs(outboundAmount) > EPSILON) {
|
||||||
|
const balanceToOutbound = this.graph.getEdgeWeight(EdgeTypes.BALANCE, citationEdge.from, citationEdge.to)
|
||||||
?? 0;
|
?? 0;
|
||||||
let refundFromOutbound = 0;
|
let refundFromOutbound = 0;
|
||||||
|
|
||||||
// Special case: Incineration.
|
// Special case: Incineration.
|
||||||
if (citationEdge.to.id === INCINERATOR_ADDRESS) {
|
if (citationEdge.to.id === INCINERATOR_ADDRESS) {
|
||||||
// Only a positive amount may be incinerated! Otherwise the sink could be used as a source.
|
// Only a positive amount may be incinerated! Otherwise the sink could be used as a source.
|
||||||
if (outboundAmount < 0) {
|
if (outboundAmount < 0) {
|
||||||
this.scene?.flowchart?.log(`style ${citationEdge.from.id} fill:#620000`);
|
this.scene?.flowchart?.log(`style ${citationEdge.from.id} fill:#620000`);
|
||||||
this.actions.propagate.log(
|
this.actions.propagate.log(
|
||||||
citationEdge.from.data,
|
citationEdge.from.data,
|
||||||
{ name: 'Incinerator' },
|
{ name: 'Incinerator' },
|
||||||
`(${increment})`,
|
incrementsStr,
|
||||||
undefined,
|
undefined,
|
||||||
'-x',
|
'-x',
|
||||||
);
|
);
|
||||||
throw new Error('Incinerator can only receive positive citations!');
|
throw new Error('Incinerator can only receive positive citations!');
|
||||||
}
|
}
|
||||||
// Reputation sent to the incinerator is burned! This means it is deducted from the sender,
|
// Reputation sent to the incinerator is burned! This means it is deducted from the sender,
|
||||||
// without increasing the value of any other token.
|
// without increasing the value of any other token.
|
||||||
this.actions.propagate.log(citationEdge.from.data, { name: 'Incinerator' }, `(${increment})`);
|
this.actions.propagate.log(citationEdge.from.data, { name: 'Incinerator' }, incrementsStr);
|
||||||
} else {
|
} else {
|
||||||
// We need to ensure that we at most undo the prior effects of this post
|
// We need to ensure that we at most undo the prior effects of this post
|
||||||
if (initialNegative) {
|
if (initialNegative) {
|
||||||
outboundAmount = outboundAmount < 0
|
outboundAmount = outboundAmount < 0
|
||||||
? Math.max(outboundAmount, -balanceToOutbound)
|
? Math.max(outboundAmount, -balanceToOutbound)
|
||||||
: Math.min(outboundAmount, -balanceToOutbound);
|
: Math.min(outboundAmount, -balanceToOutbound);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively propagate reputation effects
|
||||||
|
refundFromOutbound = await this.propagateValue(citationEdge, {
|
||||||
|
rewardsAccumulator,
|
||||||
|
increment: outboundAmount,
|
||||||
|
depth: depth + 1,
|
||||||
|
initialNegative: initialNegative || (depth === 0 && outboundAmount < 0),
|
||||||
|
referenceChainLimit,
|
||||||
|
leachingValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Any excess (negative) amount that could not be propagated,
|
||||||
|
// i.e. because a cited post has been reduced to zero value,
|
||||||
|
// is retained by the citing post.
|
||||||
|
outboundAmount -= refundFromOutbound;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively propagate reputation effects
|
// Keep a record of the effect of the reputation transferred along this edge in the graph,
|
||||||
refundFromOutbound = await this.propagateValue(citationEdge, {
|
// so that later, negative citations can be constrained to at most undo these effects.
|
||||||
rewardsAccumulator,
|
this.graph.setEdgeWeight(
|
||||||
increment: outboundAmount,
|
EdgeTypes.BALANCE,
|
||||||
depth: depth + 1,
|
citationEdge.from,
|
||||||
initialNegative: initialNegative || (depth === 0 && outboundAmount < 0),
|
citationEdge.to,
|
||||||
referenceChainLimit,
|
balanceToOutbound + outboundAmount,
|
||||||
leachingValue,
|
);
|
||||||
});
|
refundsFromOutbound[idx] = refundFromOutbound;
|
||||||
|
totalOutboundAmounts[idx] += outboundAmount;
|
||||||
// Any excess (negative) amount that could not be propagated,
|
|
||||||
// i.e. because a cited post has been reduced to zero value,
|
|
||||||
// is retained by the citing post.
|
|
||||||
outboundAmount -= refundFromOutbound;
|
|
||||||
}
|
}
|
||||||
|
const refundStr = refundsFromOutbound.map((refund) => displayNumber(refund)).join('/');
|
||||||
// Keep a record of the effect of the reputation transferred along this edge in the graph,
|
|
||||||
// so that later, negative citations can be constrained to at most undo these effects.
|
|
||||||
this.graph.setEdgeWeight(
|
|
||||||
EdgeTypes.BALANCE,
|
|
||||||
citationEdge.from,
|
|
||||||
citationEdge.to,
|
|
||||||
balanceToOutbound + outboundAmount,
|
|
||||||
);
|
|
||||||
totalOutboundAmount += outboundAmount;
|
|
||||||
|
|
||||||
this.actions.confirm.log(
|
this.actions.confirm.log(
|
||||||
citationEdge.to.data,
|
citationEdge.to.data,
|
||||||
citationEdge.from.data,
|
citationEdge.from.data,
|
||||||
`(refund: ${displayNumber(refundFromOutbound)}, leach: ${outboundAmount * leachingValue})`,
|
`(refund: ${refundStr}, leach: ${outboundAmount * leachingValue})`,
|
||||||
undefined,
|
undefined,
|
||||||
'-->>',
|
'-->>',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return totalOutboundAmount;
|
return totalOutboundAmounts;
|
||||||
};
|
};
|
||||||
|
|
||||||
// First, leach value via negative citations
|
// First, leach value via negative citations
|
||||||
const totalLeachingAmount = await propagate(false);
|
const totalLeachingAmounts = await propagate(false);
|
||||||
increment -= totalLeachingAmount * leachingValue;
|
for (let idx = 0; idx < totalLeachingAmounts.length; idx++) {
|
||||||
|
increments[idx] -= totalLeachingAmounts[idx] * leachingValue;
|
||||||
|
}
|
||||||
|
|
||||||
// Now propagate value via positive citations
|
// Now propagate value via positive citations
|
||||||
const totalDonationAmount = await propagate(true);
|
const totalDonationAmounts = await propagate(true);
|
||||||
increment -= totalDonationAmount * leachingValue;
|
for (let idx = 0; idx < totalDonationAmounts.length; idx++) {
|
||||||
|
increments[idx] -= totalDonationAmounts[idx] * leachingValue;
|
||||||
|
}
|
||||||
|
|
||||||
// Apply the remaining increment to the present post
|
// Apply the remaining increment to the present post
|
||||||
const rawNewValue = post.value + increment;
|
const rawNewValues = post.values.map((value, idx) => value + increments[idx]);
|
||||||
const newValue = Math.max(0, rawNewValue);
|
const newValues = rawNewValues.map((rawNewValue) => Math.max(0, rawNewValue));
|
||||||
const appliedIncrement = newValue - post.value;
|
const appliedIncrements = newValue - post.value;
|
||||||
const refundToInbound = increment - appliedIncrement;
|
const refundToInbound = increment - appliedIncrement;
|
||||||
|
|
||||||
// Apply reputation effects to post authors, not to the post directly
|
// Apply reputation effects to post authors, not to the post directly
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { ReputationHolder } from '../reputation/reputation-holder.js';
|
||||||
import { Stake } from '../supporting/stake.js';
|
import { Stake } from '../supporting/stake.js';
|
||||||
import { Action } from '../display/action.js';
|
import { Action } from '../display/action.js';
|
||||||
import { displayNumber } from '../../util/helpers.js';
|
import { displayNumber } from '../../util/helpers.js';
|
||||||
|
import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../util/constants.js';
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
/* Validation Pool parameters */
|
/* Validation Pool parameters */
|
||||||
|
@ -51,6 +52,7 @@ export class ValidationPool extends ReputationHolder {
|
||||||
duration,
|
duration,
|
||||||
tokenLossRatio,
|
tokenLossRatio,
|
||||||
contentiousDebate = false,
|
contentiousDebate = false,
|
||||||
|
reputationTypes,
|
||||||
},
|
},
|
||||||
name,
|
name,
|
||||||
scene,
|
scene,
|
||||||
|
@ -69,6 +71,16 @@ export class ValidationPool extends ReputationHolder {
|
||||||
this.actions.initiate.log(fromActor, this, `(fee: ${fee})`);
|
this.actions.initiate.log(fromActor, this, `(fee: ${fee})`);
|
||||||
this.activate();
|
this.activate();
|
||||||
|
|
||||||
|
// Supporting a simplified use case, if the reputation type is not specified let's use a default
|
||||||
|
this.reputationTypes = reputationTypes ?? [{ reputationTypeId: DEFAULT_REP_TOKEN_TYPE_ID, weight: 1 }];
|
||||||
|
// Normalize so reputation weights sum to 1
|
||||||
|
{
|
||||||
|
const weightTotal = this.reputationTypes.reduce((total, { weight }) => total += weight, 0);
|
||||||
|
for (const reputationType of this.reputationTypes) {
|
||||||
|
reputationType.weight /= weightTotal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If contentiousDebate = true, we will follow the progression defined by getTokenLossRatio()
|
// If contentiousDebate = true, we will follow the progression defined by getTokenLossRatio()
|
||||||
if (
|
if (
|
||||||
!contentiousDebate
|
!contentiousDebate
|
||||||
|
@ -130,22 +142,34 @@ export class ValidationPool extends ReputationHolder {
|
||||||
this.duration = duration;
|
this.duration = duration;
|
||||||
this.tokenLossRatio = tokenLossRatio;
|
this.tokenLossRatio = tokenLossRatio;
|
||||||
this.contentiousDebate = contentiousDebate;
|
this.contentiousDebate = contentiousDebate;
|
||||||
this.mintedValue = fee * params.mintingRatio();
|
|
||||||
this.tokenId = this.dao.reputation.mint(this.id, this.mintedValue);
|
|
||||||
// Tokens minted "for" the post go toward stake of author voting for their own post.
|
|
||||||
// Also, author can provide additional stakes, e.g. availability stakes for work evidence post.
|
|
||||||
this.stake(this.id, {
|
|
||||||
position: true,
|
|
||||||
amount: this.mintedValue * params.stakeForAuthor,
|
|
||||||
tokenId: this.tokenId,
|
|
||||||
});
|
|
||||||
this.stake(this.id, {
|
|
||||||
position: false,
|
|
||||||
amount: this.mintedValue * (1 - params.stakeForAuthor),
|
|
||||||
tokenId: this.tokenId,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.actions.mint.log(this, this, `(${this.mintedValue})`);
|
const mintTotal = fee * params.mintingRatio();
|
||||||
|
const reputationTypeIds = this.reputationTypes
|
||||||
|
.map(({ reputationTypeId }) => reputationTypeId);
|
||||||
|
const mintValues = this.reputationTypes
|
||||||
|
.map(({ weight }) => mintTotal * weight);
|
||||||
|
console.log('validation pool constructor', { reputationTypeIds, mintValues });
|
||||||
|
this.tokenAddress = this.dao.reputation.mintBatch(this.id, reputationTypeIds, mintValues);
|
||||||
|
this.reputationTypeIds = reputationTypeIds;
|
||||||
|
|
||||||
|
// Minted tokens are staked for/against the post at configured ratio
|
||||||
|
// Each type of reputation is staked in the proportions specified by the `reputationTypes` parameter
|
||||||
|
for (const { reputationTypeId, weight } of this.reputationTypes) {
|
||||||
|
this.stake(this.id, {
|
||||||
|
position: true,
|
||||||
|
amount: mintTotal * params.stakeForAuthor * weight,
|
||||||
|
tokenAddress: this.tokenAddress,
|
||||||
|
reputationTypeId,
|
||||||
|
});
|
||||||
|
this.stake(this.id, {
|
||||||
|
position: false,
|
||||||
|
amount: this.mintedValue * (1 - params.stakeForAuthor) * weight,
|
||||||
|
tokenAddress: this.tokenAddress,
|
||||||
|
reputationTypeId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.actions.mint.log(this, this, `(${mintTotal})`);
|
||||||
|
|
||||||
// Keep a record of voters and their votes
|
// Keep a record of voters and their votes
|
||||||
this.dao.addVoteRecord(reputationPublicKey, this);
|
this.dao.addVoteRecord(reputationPublicKey, this);
|
||||||
|
@ -174,39 +198,38 @@ export class ValidationPool extends ReputationHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
|
* @param {boolean} options.outcome: null --> all entries. Otherwise filters to position === outcome.
|
||||||
|
* @param {boolean} options.tokenTypeId: null --> all entries. Otherwise filters to the given token type.
|
||||||
* @param {boolean} options.excludeSystem: Whether to exclude votes cast during pool initialization
|
* @param {boolean} options.excludeSystem: Whether to exclude votes cast during pool initialization
|
||||||
* @returns stake[]
|
* @returns stake[]
|
||||||
*/
|
*/
|
||||||
getStakes(outcome, { excludeSystem }) {
|
getStakes({ outcome, tokenTypeId, excludeSystem }) {
|
||||||
return Array.from(this.stakes.values())
|
return Array.from(this.stakes.values())
|
||||||
.filter(({ tokenId }) => !excludeSystem || tokenId !== this.tokenId)
|
.filter((stake) => tokenTypeId === null || stake.tokenTypeId === tokenTypeId)
|
||||||
|
.filter(({ tokenAddress }) => !excludeSystem || tokenAddress !== this.tokenAddress)
|
||||||
.filter(({ position }) => outcome === null || position === outcome);
|
.filter(({ position }) => outcome === null || position === outcome);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
|
* @param {boolean} options.outcome: null --> all entries. Otherwise filters to position === outcome.
|
||||||
* @returns number
|
* @returns number
|
||||||
*/
|
*/
|
||||||
getTotalStakedOnPost(outcome) {
|
getStakedAmount({ outcome, tokenTypeId }) {
|
||||||
return this.getStakes(outcome, { excludeSystem: false })
|
return this.getStakes({ outcome, tokenTypeId, excludeSystem: false })
|
||||||
.map((stake) => stake.getStakeValue({ lockingTimeExponent: params.lockingTimeExponent }))
|
.map((stake) => stake.getAmount({ lockingTimeExponent: params.lockingTimeExponent }))
|
||||||
.reduce((acc, cur) => (acc += cur), 0);
|
.reduce((total, amount) => (total += amount), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
|
* Stake reputation in favor of a given outcome for this validation pool.
|
||||||
* @returns number
|
*
|
||||||
|
* @param {*} reputationPublicKey
|
||||||
|
* @param {object} opts
|
||||||
*/
|
*/
|
||||||
getTotalValueOfStakesForOutcome(outcome) {
|
|
||||||
return this.getStakes(outcome, { excludeSystem: false })
|
|
||||||
.reduce((total, { amount }) => (total += amount), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: This can be handled as a hook on receipt of reputation token transfer
|
|
||||||
async stake(reputationPublicKey, {
|
async stake(reputationPublicKey, {
|
||||||
tokenId, position, amount, lockingTime = 0,
|
tokenAddress, tokenTypeId = DEFAULT_REP_TOKEN_TYPE_ID, position, amount, lockingTime = 0,
|
||||||
}) {
|
}) {
|
||||||
|
// TODO: This can be handled as a hook on receipt of reputation token transfer
|
||||||
if (this.state === ValidationPoolStates.CLOSED) {
|
if (this.state === ValidationPoolStates.CLOSED) {
|
||||||
throw new Error(`Validation pool ${this.id} is closed.`);
|
throw new Error(`Validation pool ${this.id} is closed.`);
|
||||||
}
|
}
|
||||||
|
@ -217,17 +240,17 @@ export class ValidationPool extends ReputationHolder {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reputationPublicKey !== this.dao.reputation.ownerOf(tokenId)) {
|
if (reputationPublicKey !== this.dao.reputation.ownerOf(tokenAddress)) {
|
||||||
throw new Error('Reputation may only be staked by its owner!');
|
throw new Error('Reputation may only be staked by its owner!');
|
||||||
}
|
}
|
||||||
|
|
||||||
const stake = new Stake({
|
const stake = new Stake({
|
||||||
tokenId, position, amount, lockingTime,
|
tokenAddress, tokenTypeId, position, amount, lockingTime,
|
||||||
});
|
});
|
||||||
this.stakes.add(stake);
|
this.stakes.add(stake);
|
||||||
|
|
||||||
// Transfer staked amount from the sender to the validation pool
|
// Transfer staked amount from the sender to the validation pool
|
||||||
this.dao.reputation.transferValueFrom(tokenId, this.tokenId, amount);
|
this.dao.reputation.transferValueFrom(tokenAddress, this.tokenAddress, tokenTypeId, amount);
|
||||||
|
|
||||||
// Keep a record of voters and their votes
|
// Keep a record of voters and their votes
|
||||||
if (reputationPublicKey !== this.id) {
|
if (reputationPublicKey !== this.id) {
|
||||||
|
@ -243,8 +266,10 @@ export class ValidationPool extends ReputationHolder {
|
||||||
// Before evaluating the winning conditions,
|
// Before evaluating the winning conditions,
|
||||||
// we need to make sure any staked tokens are locked for the
|
// we need to make sure any staked tokens are locked for the
|
||||||
// specified amounts of time.
|
// specified amounts of time.
|
||||||
for (const { tokenId, amount, lockingTime } of this.stakes.values()) {
|
for (const {
|
||||||
this.dao.reputation.lock(tokenId, amount, lockingTime);
|
tokenAddress, tokenTypeId, amount, lockingTime,
|
||||||
|
} of this.stakes.values()) {
|
||||||
|
this.dao.reputation.lock(tokenAddress, tokenTypeId, amount, lockingTime);
|
||||||
// TODO: If there is an exception here, the voter may have voted incorrectly. Consider penalties.
|
// TODO: If there is an exception here, the voter may have voted incorrectly. Consider penalties.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -261,23 +286,33 @@ export class ValidationPool extends ReputationHolder {
|
||||||
this.state = ValidationPoolStates.CLOSED;
|
this.state = ValidationPoolStates.CLOSED;
|
||||||
this.setStatus('Closed');
|
this.setStatus('Closed');
|
||||||
|
|
||||||
const upvoteValue = this.getTotalValueOfStakesForOutcome(true);
|
// Votes should be scaled by weights of this.reputationTypes
|
||||||
const downvoteValue = this.getTotalValueOfStakesForOutcome(false);
|
const upvoteValue = this.reputationTypes.reduce((value, { reputationTypeId, weight }) => {
|
||||||
const activeAvailableReputation = this.dao.getActiveAvailableReputation();
|
value += this.getStakedAmount({ outcome: true, reputationTypeId }) * weight;
|
||||||
const votePasses = upvoteValue >= params.winningRatio * downvoteValue;
|
return value;
|
||||||
|
}, 0);
|
||||||
|
const downvoteValue = this.reputationTypes.reduce((value, { reputationTypeId, weight }) => {
|
||||||
|
value += this.getStakedAmount({ outcome: false, reputationTypeId }) * weight;
|
||||||
|
return value;
|
||||||
|
}, 0);
|
||||||
|
const activeAvailableReputation = this.reputationTypes.reduce((value, { reputationTypeId, weight }) => {
|
||||||
|
value += this.dao.getActiveAvailableReputation({ reputationTypeId }) * weight;
|
||||||
|
return value;
|
||||||
|
}, 0);
|
||||||
|
const outcome = upvoteValue >= params.winningRatio * downvoteValue;
|
||||||
const quorumMet = upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation;
|
const quorumMet = upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation;
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
votePasses,
|
outcome,
|
||||||
upvoteValue,
|
upvoteValue,
|
||||||
downvoteValue,
|
downvoteValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (quorumMet) {
|
if (quorumMet) {
|
||||||
this.setStatus(`Resolved - ${votePasses ? 'Won' : 'Lost'}`);
|
this.setStatus(`Resolved - ${outcome ? 'Won' : 'Lost'}`);
|
||||||
this.scene?.sequence.log(`note over ${this.name} : ${votePasses ? 'Win' : 'Lose'}`);
|
this.scene?.sequence.log(`note over ${this.name} : ${outcome ? 'Win' : 'Lose'}`);
|
||||||
this.applyTokenLocking();
|
this.applyTokenLocking();
|
||||||
await this.distributeReputation({ votePasses });
|
await this.distributeReputation({ outcome });
|
||||||
// TODO: distribute fees
|
// TODO: distribute fees
|
||||||
} else {
|
} else {
|
||||||
this.setStatus('Resolved - Quorum not met');
|
this.setStatus('Resolved - Quorum not met');
|
||||||
|
@ -301,47 +336,57 @@ export class ValidationPool extends ReputationHolder {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async distributeReputation({ votePasses }) {
|
async distributeReputation({ outcome }) {
|
||||||
// For now we assume a tightly binding pool, where all staked reputation is lost
|
// In a binding validation pool, losing voter stakes are transferred to winning voters.
|
||||||
// TODO: Take tokenLossRatio into account
|
// TODO: Regression tests for different tokenLossRatio values
|
||||||
// TODO: revoke staked reputation from losing voters
|
const tokenLossRatio = this.getTokenLossRatio();
|
||||||
|
for (const { reputationTypeId, weight } of this.reputationTypes) {
|
||||||
|
const tokensForWinners = this.getStakedAmount({ outcome: !outcome, reputationTypeId }) * weight * tokenLossRatio;
|
||||||
|
const winningEntries = this.getStakes({ outcome, reputationTypeId, excludeSystem: true });
|
||||||
|
const totalValueOfStakesForWin = this.getStakedAmount({ outcome, reputationTypeId });
|
||||||
|
|
||||||
// In a tightly binding validation pool, losing voter stakes are transferred to winning voters.
|
// Compute rewards for the winning voters, in proportion to the value of their stakes.
|
||||||
const tokensForWinners = this.getTotalStakedOnPost(!votePasses);
|
for (const stake of winningEntries) {
|
||||||
const winningEntries = this.getStakes(votePasses, { excludeSystem: true });
|
const { tokenAddress, amount } = stake;
|
||||||
const totalValueOfStakesForWin = this.getTotalValueOfStakesForOutcome(votePasses);
|
const value = stake.getAmount({ lockingTimeExponent: params.lockingTimeExponent });
|
||||||
|
const reward = tokensForWinners * (value / totalValueOfStakesForWin);
|
||||||
// Compute rewards for the winning voters, in proportion to the value of their stakes.
|
// Also return each winning voter their staked amount
|
||||||
for (const stake of winningEntries) {
|
const reputationPublicKey = this.dao.reputation.ownerOf(tokenAddress);
|
||||||
const { tokenId, amount } = stake;
|
console.log(`reward of type ${reputationTypeId} for winning stake by ${reputationPublicKey}: ${reward}`);
|
||||||
const value = stake.getStakeValue({ lockingTimeExponent: params.lockingTimeExponent });
|
this.dao.reputation.transferValueFrom(this.tokenAddress, tokenAddress, reputationTypeId, reward + amount);
|
||||||
const reward = tokensForWinners * (value / totalValueOfStakesForWin);
|
const toActor = this.scene?.findActor((actor) => actor.reputationPublicKey === reputationPublicKey);
|
||||||
// Also return each winning voter their staked amount
|
this.actions.reward.log(this, toActor, `(${displayNumber(reward)} type ${reputationTypeId})`);
|
||||||
const reputationPublicKey = this.dao.reputation.ownerOf(tokenId);
|
}
|
||||||
console.log(`reward for winning stake by ${reputationPublicKey}: ${reward}`);
|
|
||||||
this.dao.reputation.transferValueFrom(this.tokenId, tokenId, reward + amount);
|
|
||||||
const toActor = this.scene?.findActor((actor) => actor.reputationPublicKey === reputationPublicKey);
|
|
||||||
this.actions.reward.log(this, toActor, `(${displayNumber(reward)})`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (votePasses) {
|
if (outcome === true) {
|
||||||
// Distribute awards to author via the forum
|
// Distribute awards to author via the forum
|
||||||
// const tokensForAuthor = this.mintedValue * params.stakeForAuthor + rewards.get(this.tokenId);
|
const tokens = this.reputationTypes.reduce((values, { reputationTypeId }) => {
|
||||||
console.log(`sending reward for author stake to forum: ${this.dao.reputation.valueOf(this.tokenId)}`);
|
const value = this.dao.reputation.valueOf(this.tokenAddress, reputationTypeId) ?? 0;
|
||||||
|
values[reputationTypeId] = value;
|
||||||
|
return values;
|
||||||
|
}, {});
|
||||||
|
console.log('sending reward for author stake to forum', tokens);
|
||||||
|
|
||||||
// Transfer ownership of the minted token, from the pool to the forum
|
// Transfer ownership of the minted token, from the pool to the forum
|
||||||
this.dao.reputation.transfer(this.id, this.dao.forum.id, this.tokenId);
|
this.dao.reputation.transfer(this.id, this.dao.forum.id, this.tokenAddress);
|
||||||
// const value = this.dao.reputation.valueOf(this.tokenId);
|
// const value = this.dao.reputation.valueOf(this.tokenAddress);
|
||||||
// this.actions.transfer.log(this, this.dao.forum, `(${value})`);
|
// this.actions.transfer.log(this, this.dao.forum, `(${value})`);
|
||||||
|
|
||||||
// Recurse through forum to determine reputation effects
|
const result = {
|
||||||
await this.dao.forum.onValidate({
|
|
||||||
pool: this,
|
pool: this,
|
||||||
postId: this.postId,
|
postId: this.postId,
|
||||||
tokenId: this.tokenId,
|
tokenAddress: this.tokenAddress,
|
||||||
referenceChainLimit: params.referenceChainLimit,
|
referenceChainLimit: params.referenceChainLimit,
|
||||||
leachingValue: params.leachingValue,
|
leachingValue: params.leachingValue,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// Recurse through forum to determine reputation effects
|
||||||
|
await this.dao.forum.onValidate({ ...result });
|
||||||
|
|
||||||
|
if (this.onValidate) {
|
||||||
|
await this.onValidate({ ...result });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('pool complete');
|
console.log('pool complete');
|
||||||
|
|
|
@ -89,6 +89,9 @@ export class Actor {
|
||||||
async computeDisplayValues(cb) {
|
async computeDisplayValues(cb) {
|
||||||
for (const [label, fn] of this.valueFunctions.entries()) {
|
for (const [label, fn] of this.valueFunctions.entries()) {
|
||||||
const value = fn();
|
const value = fn();
|
||||||
|
console.log('computeDisplay', {
|
||||||
|
label, value, fn, cb,
|
||||||
|
});
|
||||||
await this.setDisplayValue(label, value);
|
await this.setDisplayValue(label, value);
|
||||||
if (cb) {
|
if (cb) {
|
||||||
cb(label, value);
|
cb(label, value);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { DisplayValue } from './display-value.js';
|
import { DisplayValue } from './display-value.js';
|
||||||
import { randomID } from '../../util/helpers.js';
|
import { randomID } from '../../util/helpers.js';
|
||||||
|
import { Rectangle, Vector } from '../supporting/geometry/index.js';
|
||||||
|
|
||||||
export class Box {
|
export class Box {
|
||||||
constructor(name, parentEl, options = {}) {
|
constructor(name, parentEl, options = {}) {
|
||||||
|
@ -20,6 +21,7 @@ export class Box {
|
||||||
parentEl.appendChild(this.el);
|
parentEl.appendChild(this.el);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.position = options.position ?? Vector.zeros(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
flex({ center = false } = {}) {
|
flex({ center = false } = {}) {
|
||||||
|
@ -35,11 +37,6 @@ export class Box {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
hidden() {
|
|
||||||
this.addClass('hidden');
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
addClass(className) {
|
addClass(className) {
|
||||||
this.el.classList.add(className);
|
this.el.classList.add(className);
|
||||||
return this;
|
return this;
|
||||||
|
@ -63,4 +60,19 @@ export class Box {
|
||||||
getId() {
|
getId() {
|
||||||
return this.el.id;
|
return this.el.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get rect() {
|
||||||
|
const {
|
||||||
|
width, height,
|
||||||
|
} = this.el.getBoundingClientRect();
|
||||||
|
return new Rectangle(this.position, [width, height]);
|
||||||
|
}
|
||||||
|
|
||||||
|
move(vector) {
|
||||||
|
vector = vector instanceof Vector ? vector : Vector.from(vector);
|
||||||
|
this.position = this.position.add(vector);
|
||||||
|
this.el.style.left = `${Math.floor(this.position[0])}px`;
|
||||||
|
this.el.style.top = `${Math.floor(this.position[1])}px`;
|
||||||
|
// this.el.dispatchEvent(new CustomEvent('moved'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,6 @@ export class Document extends Box {
|
||||||
}
|
}
|
||||||
|
|
||||||
get lastElement() {
|
get lastElement() {
|
||||||
if (!this.elements.length) return null;
|
|
||||||
return this.elements[this.elements.length - 1];
|
return this.elements[this.elements.length - 1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,354 @@
|
||||||
|
import {
|
||||||
|
DEFAULT_TIME_STEP,
|
||||||
|
MINIMUM_VELOCITY,
|
||||||
|
VISCOSITY_FACTOR,
|
||||||
|
TIME_DILATION_FACTOR,
|
||||||
|
MAXIMUM_STEPS,
|
||||||
|
TRANSLATION_VELOCITY_FACTOR,
|
||||||
|
ARROWHEAD_LENGTH,
|
||||||
|
ARROWHEAD_WIDTH,
|
||||||
|
MINIMUM_STEPS,
|
||||||
|
CENTRAL_RESTORING_FORCE,
|
||||||
|
} from '../../util/constants.js';
|
||||||
|
import { Edge } from '../supporting/edge.js';
|
||||||
|
import { WeightedDirectedGraph } from '../supporting/wdg.js';
|
||||||
|
import { Box } from './box.js';
|
||||||
|
import { Vector, Rectangle } from '../supporting/geometry/index.js';
|
||||||
|
import { overlapRepulsionForce, targetRadiusForce } from './pairwise-forces.js';
|
||||||
|
import { Vertex } from '../supporting/vertex.js';
|
||||||
|
|
||||||
|
// Render children with absolute css positioning.
|
||||||
|
|
||||||
|
// Let there be a force between elements such that the force between
|
||||||
|
// any two elements is along the line between their centers,
|
||||||
|
// so that the elements repel when too close but attract when too far.
|
||||||
|
|
||||||
|
// The equilibrium distance can be tuned, e.g. can be scaled by an input.
|
||||||
|
|
||||||
|
// NOTE: (with optional overlay preferring a grid or some other shape?),
|
||||||
|
|
||||||
|
// NOTE: Could also allow user input dragging elements.
|
||||||
|
// What might be neat here is to implement a force-based resistance effect;
|
||||||
|
// basically, the mouse pointer drags the element with a spring rather than directly.
|
||||||
|
// If the shape of the graph resists the transformation,
|
||||||
|
// the distance between the element and the cursor should increase.
|
||||||
|
|
||||||
|
// On an interval, compute forces among the elements.
|
||||||
|
// Simulate the effects of these forces
|
||||||
|
|
||||||
|
// NOTE: Impart random nudges, and resolve their effects to a user-visible resolution
|
||||||
|
// before rendering.
|
||||||
|
|
||||||
|
// NOTE: When mouse is in our box, we could hijack the scroll actions to zoom in/out.
|
||||||
|
|
||||||
|
export class ForceDirectedGraph extends WeightedDirectedGraph {
|
||||||
|
constructor(name, parentEl, { width = 800, height = 600, ...options } = {}) {
|
||||||
|
super(name, options);
|
||||||
|
this.box = new Box(name, parentEl, options);
|
||||||
|
this.box.addClass('fixed');
|
||||||
|
this.box.addClass('force-directed-graph');
|
||||||
|
this.intervalTask = null;
|
||||||
|
this.canvas = window.document.createElement('canvas');
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.box.el.style.width = `${width}px`;
|
||||||
|
this.box.el.style.height = `${height}px`;
|
||||||
|
this.box.el.appendChild(this.canvas);
|
||||||
|
this.fitCanvasToGraph();
|
||||||
|
this.nodes = [];
|
||||||
|
this.edges = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
fitCanvasToGraph() {
|
||||||
|
[this.canvas.width, this.canvas.height] = this.box.rect.dimensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
addVertex(...args) {
|
||||||
|
const vertex = super.addVertex(...args);
|
||||||
|
const box = this.box.addBox(vertex.id);
|
||||||
|
|
||||||
|
// Link from the graph vertex to the corresponding display box
|
||||||
|
vertex.box = box;
|
||||||
|
|
||||||
|
// Link from the display box to the corresponding graph vertex
|
||||||
|
box.vertex = vertex;
|
||||||
|
|
||||||
|
box.addClass('absolute');
|
||||||
|
box.addClass('vertex');
|
||||||
|
box.move(this.box.rect.dimensions.scale(0.5));
|
||||||
|
box.velocity = Vector.from([0, 0]);
|
||||||
|
box.setInnerHTML(vertex.getHTML());
|
||||||
|
this.nodes.push(box);
|
||||||
|
|
||||||
|
// When vertex properties are updated, re-render the node contents
|
||||||
|
vertex.onUpdate = () => {
|
||||||
|
box.setInnerHTML(vertex.getHTML());
|
||||||
|
// Maybe resolve forces
|
||||||
|
this.runUntilEquilibrium();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.runUntilEquilibrium();
|
||||||
|
|
||||||
|
// Allow moving vertices with the mouse
|
||||||
|
box.el.addEventListener('mousedown', (e) => {
|
||||||
|
console.log('mousedown, button:', e.button);
|
||||||
|
if (!this.mouseMoving) {
|
||||||
|
e.preventDefault();
|
||||||
|
// Record current mouse position
|
||||||
|
this.mousePosition = Vector.from([e.clientX, e.clientY]);
|
||||||
|
// Begin tracking mouse movement
|
||||||
|
this.mouseMoving = box;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.addEventListener('mousemove', (e) => {
|
||||||
|
if (this.mouseMoving === box) {
|
||||||
|
const mousePosition = Vector.from([e.clientX, e.clientY]);
|
||||||
|
// Apply translation
|
||||||
|
box.move(mousePosition.subtract(this.mousePosition));
|
||||||
|
// Update current mouse position
|
||||||
|
this.mousePosition = mousePosition;
|
||||||
|
// Equilibrate
|
||||||
|
this.runUntilEquilibrium();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.addEventListener('mouseup', () => {
|
||||||
|
// Stop tracking mouse movement
|
||||||
|
this.mouseMoving = null;
|
||||||
|
// Equilibrate
|
||||||
|
this.runUntilEquilibrium();
|
||||||
|
});
|
||||||
|
|
||||||
|
return vertex;
|
||||||
|
}
|
||||||
|
|
||||||
|
addEdge(type, from, to, ...rest) {
|
||||||
|
from = from instanceof Vertex ? from : this.getVertex(from);
|
||||||
|
to = to instanceof Vertex ? to : this.getVertex(to);
|
||||||
|
if (!from) throw new Error(`from ${from}: Node not found`);
|
||||||
|
if (!to) throw new Error(`to ${to}: Node not found`);
|
||||||
|
|
||||||
|
const edge = super.addEdge(type, from, to, ...rest);
|
||||||
|
const box = this.box.addBox(Edge.getKey({ from, to, type }));
|
||||||
|
|
||||||
|
edge.box = box;
|
||||||
|
box.edge = edge;
|
||||||
|
|
||||||
|
box.addClass('absolute');
|
||||||
|
box.addClass('edge');
|
||||||
|
box.setInnerHTML(edge.getHTML());
|
||||||
|
this.edges.push(box);
|
||||||
|
|
||||||
|
// Initially place near the midpoint between the `from` and `to` nodes.
|
||||||
|
const midpoint = from.box.rect.center.add(to.box.rect.center).scale(0.5);
|
||||||
|
const startPosition = midpoint.subtract(box.rect.dimensions.scale(0.5));
|
||||||
|
box.move(startPosition);
|
||||||
|
box.velocity = Vector.from([0, 0]);
|
||||||
|
this.runUntilEquilibrium();
|
||||||
|
|
||||||
|
return edge;
|
||||||
|
}
|
||||||
|
|
||||||
|
setEdgeWeight(...args) {
|
||||||
|
const edge = super.setEdgeWeight(...args);
|
||||||
|
edge.displayEdgeNode();
|
||||||
|
return edge;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawEdgeLines() {
|
||||||
|
const ctx = this.canvas.getContext('2d');
|
||||||
|
ctx.clearRect(0, 0, this.width, this.height);
|
||||||
|
ctx.strokeStyle = '#57747d';
|
||||||
|
ctx.fillStyle = '#57747d';
|
||||||
|
for (const { edge } of this.edges) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(...edge.from.box.rect.center);
|
||||||
|
ctx.lineTo(...edge.box.rect.center);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
const direction = edge.to.box.rect.center.subtract(edge.box.rect.center);
|
||||||
|
const arrowTail = edge.box.rect.lineIntersect(edge.to.box.rect.center, direction.scale(-1));
|
||||||
|
const arrowPoint = edge.to.box.rect.lineIntersect(edge.box.rect.center, direction);
|
||||||
|
const arrowBaseCenter = arrowPoint.subtract(direction.normalize().scale(ARROWHEAD_LENGTH));
|
||||||
|
const arrowBaseDirection = Vector.from([direction[1], -direction[0]]).normalize();
|
||||||
|
const arrowBaseLeft = arrowBaseCenter.add(arrowBaseDirection.scale(ARROWHEAD_WIDTH));
|
||||||
|
const arrowBaseRight = arrowBaseCenter.subtract(arrowBaseDirection.scale(ARROWHEAD_WIDTH));
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(...arrowTail);
|
||||||
|
ctx.lineTo(...arrowPoint);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(...arrowPoint);
|
||||||
|
ctx.lineTo(...arrowBaseLeft);
|
||||||
|
ctx.lineTo(...arrowBaseRight);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async runUntilEquilibrium(tDelta = DEFAULT_TIME_STEP) {
|
||||||
|
this.steps = 0;
|
||||||
|
if (this.intervalTask) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.intervalTask = setInterval(() => {
|
||||||
|
if (this.intervalTaskExecuting) return;
|
||||||
|
this.steps++;
|
||||||
|
if (this.steps > MAXIMUM_STEPS) {
|
||||||
|
clearInterval(this.intervalTask);
|
||||||
|
this.intervalTask = null;
|
||||||
|
reject(new Error('Exceeded max steps to reach equilibrium'));
|
||||||
|
}
|
||||||
|
this.intervalTaskExecuting = true;
|
||||||
|
const { atEquilibrium } = this.computeEulerFrame(tDelta);
|
||||||
|
this.intervalTaskExecuting = false;
|
||||||
|
if (atEquilibrium) {
|
||||||
|
console.log(`Reached equilibrium after ${this.steps} steps`);
|
||||||
|
clearInterval(this.intervalTask);
|
||||||
|
this.intervalTask = null;
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}, tDelta * TIME_DILATION_FACTOR);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
computeEulerFrame(tDelta = DEFAULT_TIME_STEP) {
|
||||||
|
// Compute net forces on each box in the graph
|
||||||
|
const boxes = [...this.nodes, ...this.edges];
|
||||||
|
|
||||||
|
// Initialize net force vectors
|
||||||
|
for (const box of boxes) {
|
||||||
|
box.forces = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const addForce = (box, force, type) => box.forces.push({ force, type });
|
||||||
|
|
||||||
|
// All boxes repel each other if they overlap
|
||||||
|
for (const boxA of boxes) {
|
||||||
|
const idxA = boxes.indexOf(boxA);
|
||||||
|
for (const boxB of boxes.slice(idxA + 1)) {
|
||||||
|
const force = overlapRepulsionForce(boxA, boxB);
|
||||||
|
addForce(
|
||||||
|
boxA,
|
||||||
|
force.scale(-1),
|
||||||
|
`${boxB.name} -- overlapRepulsion --> ${boxA.name}`,
|
||||||
|
);
|
||||||
|
addForce(
|
||||||
|
boxB,
|
||||||
|
force,
|
||||||
|
`${boxA.name} -- overlapRepulsion --> ${boxB.name}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center of graph attracts all boxes that are outside the graph
|
||||||
|
for (const box of boxes) {
|
||||||
|
if (!this.box.rect.doesContain(box.rect.center)) {
|
||||||
|
const r = this.box.rect.center.subtract(box.rect.center);
|
||||||
|
addForce(
|
||||||
|
box,
|
||||||
|
r.normalize().scale(CENTRAL_RESTORING_FORCE),
|
||||||
|
`center -- attraction --> ${box.name}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute edge-related forces
|
||||||
|
for (const edgeBox of this.edges) {
|
||||||
|
const { edge } = edgeBox;
|
||||||
|
const fromBox = edge.from.box;
|
||||||
|
const toBox = edge.to.box;
|
||||||
|
|
||||||
|
// Attraction to the `from` and `to` nodes
|
||||||
|
addForce(
|
||||||
|
edgeBox,
|
||||||
|
targetRadiusForce(fromBox, edgeBox, 0),
|
||||||
|
`${edgeBox.name} -- attract fromBox --> ${fromBox.name}`,
|
||||||
|
);
|
||||||
|
addForce(
|
||||||
|
edgeBox,
|
||||||
|
targetRadiusForce(toBox, edgeBox, 0),
|
||||||
|
`${edgeBox.name} -- attract toBox --> ${toBox.name}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pairwise force between nodes
|
||||||
|
{
|
||||||
|
const force = targetRadiusForce(fromBox, toBox);
|
||||||
|
addForce(
|
||||||
|
fromBox,
|
||||||
|
force.scale(-1),
|
||||||
|
`${toBox.name} -- pairwise targetRadius --> ${fromBox.name}`,
|
||||||
|
);
|
||||||
|
addForce(
|
||||||
|
toBox,
|
||||||
|
force,
|
||||||
|
`${fromBox.name} -- pairwise targetRadius --> ${toBox.name}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not apply forces to a box if it is being moved by the mouse
|
||||||
|
for (const box of boxes) {
|
||||||
|
if (this.mouseMoving === box) {
|
||||||
|
box.netForce = Vector.zeros(2);
|
||||||
|
} else {
|
||||||
|
box.netForce = box.forces.reduce((net, { force }) => net.add(force), Vector.from([0, 0]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute velocities
|
||||||
|
for (const box of boxes) {
|
||||||
|
box.velocity = box.velocity.add(box.netForce.scale(tDelta));
|
||||||
|
// Apply some drag
|
||||||
|
box.velocity = box.velocity.scale(1 - VISCOSITY_FACTOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When all velocities are below MINIMUM_VELOCITY and we have executed more than
|
||||||
|
// MINIMUM_STEPS, we have reached equilibrium.
|
||||||
|
let atEquilibrium = this.steps > MINIMUM_STEPS;
|
||||||
|
|
||||||
|
// Apply velocities
|
||||||
|
for (const box of boxes) {
|
||||||
|
if (box.velocity.magnitude >= MINIMUM_VELOCITY) {
|
||||||
|
atEquilibrium = false;
|
||||||
|
box.move(box.velocity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center the items by computing the bounding box and centering that
|
||||||
|
if (!this.mouseMoving) {
|
||||||
|
const topLeft = Vector.from(boxes[0].position);
|
||||||
|
const bottomRight = Vector.from(boxes[0].position);
|
||||||
|
for (const box of boxes) {
|
||||||
|
for (const vertex of box.rect.vertices) {
|
||||||
|
topLeft[0] = Math.min(topLeft[0], vertex[0]);
|
||||||
|
topLeft[1] = Math.min(topLeft[1], vertex[1]);
|
||||||
|
|
||||||
|
bottomRight[0] = Math.max(bottomRight[0], vertex[0]);
|
||||||
|
bottomRight[1] = Math.max(bottomRight[1], vertex[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const boundingRect = new Rectangle(topLeft, bottomRight.subtract(topLeft));
|
||||||
|
const graphCenter = Vector.from([this.width, this.height]).scale(0.5);
|
||||||
|
const offset = graphCenter.subtract(boundingRect.center);
|
||||||
|
const translate = offset.scale(TRANSLATION_VELOCITY_FACTOR);
|
||||||
|
|
||||||
|
if (translate.magnitude >= MINIMUM_VELOCITY) {
|
||||||
|
atEquilibrium = false;
|
||||||
|
|
||||||
|
// Apply translations
|
||||||
|
for (const box of boxes) {
|
||||||
|
box.move(translate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Scaling to fit
|
||||||
|
|
||||||
|
this.drawEdgeLines();
|
||||||
|
|
||||||
|
return { atEquilibrium };
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,20 @@ export class FormElement extends Box {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class Select extends FormElement {
|
||||||
|
constructor(name, form, opts) {
|
||||||
|
super(name, form, opts);
|
||||||
|
const { options } = opts;
|
||||||
|
this.selectEl = document.createElement('select');
|
||||||
|
for (const { value, label } of options) {
|
||||||
|
const optionEl = document.createElement('option');
|
||||||
|
optionEl.setAttribute('value', value);
|
||||||
|
optionEl.innerHTML = label || value;
|
||||||
|
}
|
||||||
|
this.el.appendChild(this.selectEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class Button extends FormElement {
|
export class Button extends FormElement {
|
||||||
constructor(name, form, opts) {
|
constructor(name, form, opts) {
|
||||||
super(name, form, { ...opts, cbEventTypes: ['click'] });
|
super(name, form, { ...opts, cbEventTypes: ['click'] });
|
||||||
|
@ -43,15 +57,18 @@ export class TextField extends FormElement {
|
||||||
constructor(name, form, opts) {
|
constructor(name, form, opts) {
|
||||||
super(name, form, opts);
|
super(name, form, opts);
|
||||||
this.flex({ center: true });
|
this.flex({ center: true });
|
||||||
this.label = document.createElement('label');
|
|
||||||
this.labelDiv = document.createElement('div');
|
// Place label inside a div, for improved styling
|
||||||
this.label.appendChild(this.labelDiv);
|
const labelDiv = document.createElement('div');
|
||||||
this.labelDiv.innerHTML = opts.label || name;
|
labelDiv.innerHTML = opts.label || name;
|
||||||
this.input = document.createElement('input');
|
const label = document.createElement('label');
|
||||||
this.input.disabled = !!opts.disabled;
|
label.appendChild(labelDiv);
|
||||||
this.input.defaultValue = opts.defaultValue || '';
|
const input = document.createElement('input');
|
||||||
this.label.appendChild(this.input);
|
input.disabled = !!opts.disabled;
|
||||||
this.el.appendChild(this.label);
|
input.defaultValue = opts.defaultValue || '';
|
||||||
|
label.appendChild(input);
|
||||||
|
this.el.appendChild(label);
|
||||||
|
this.input = input;
|
||||||
}
|
}
|
||||||
|
|
||||||
get value() {
|
get value() {
|
||||||
|
@ -61,6 +78,30 @@ export class TextField extends FormElement {
|
||||||
|
|
||||||
export class TextArea extends FormElement { }
|
export class TextArea extends FormElement { }
|
||||||
|
|
||||||
|
export class SubForm extends FormElement {
|
||||||
|
// Form has:
|
||||||
|
// this.document = document;
|
||||||
|
// this.items = [];
|
||||||
|
// this.id = opts.id ?? `form_${randomID()}`;
|
||||||
|
// FormElement has
|
||||||
|
constructor(name, form, opts) {
|
||||||
|
if (!name) {
|
||||||
|
name = `subform${randomID()}`;
|
||||||
|
}
|
||||||
|
const parentEl = opts.subFormArray ? opts.subFormArray.el : form.el;
|
||||||
|
const subForm = form.document.form({ name, parentEl, tagName: 'div' }).lastElement;
|
||||||
|
super(name, form, { ...opts, parentEl });
|
||||||
|
this.subForm = subForm;
|
||||||
|
if (opts.subFormArray) {
|
||||||
|
opts.subFormArray.subForms.push(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this.subForm.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class SubFormArray extends FormElement {
|
export class SubFormArray extends FormElement {
|
||||||
constructor(name, form, opts) {
|
constructor(name, form, opts) {
|
||||||
super(name, form, opts);
|
super(name, form, opts);
|
||||||
|
@ -76,36 +117,30 @@ export class SubFormArray extends FormElement {
|
||||||
this.subForms.splice(idx, 1);
|
this.subForms.splice(idx, 1);
|
||||||
subForm.el.remove();
|
subForm.el.remove();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export class SubForm extends FormElement {
|
subForm(opts = {}) {
|
||||||
constructor(name, form, opts) {
|
const subForm = new SubForm(opts.name, this.form, { ...opts, subFormArray: this });
|
||||||
const parentEl = opts.subFormArray ? opts.subFormArray.el : form.el;
|
this.subForms.push(subForm);
|
||||||
const subForm = form.document.form({ name, parentEl, tagName: 'div' }).lastElement;
|
return this;
|
||||||
super(name, form, { ...opts, parentEl });
|
|
||||||
this.subForm = subForm;
|
|
||||||
if (opts.subFormArray) {
|
|
||||||
opts.subFormArray.subForms.push(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get value() {
|
get lastSubForm() {
|
||||||
return this.subForm.value;
|
return this.subForms[this.subForms.length - 1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FileInput extends FormElement {
|
export class FileInput extends FormElement {
|
||||||
constructor(name, form, opts) {
|
constructor(name, form, opts) {
|
||||||
super(name, form, opts);
|
super(name, form, opts);
|
||||||
this.input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
this.input.type = 'file';
|
input.type = 'file';
|
||||||
this.input.accept = 'application/json';
|
input.accept = 'application/json';
|
||||||
this.input.classList.add('visually-hidden');
|
input.classList.add('visually-hidden');
|
||||||
this.label = document.createElement('label');
|
const label = document.createElement('label');
|
||||||
this.button = form.button({ name, cb: () => this.input.click() }).lastItem;
|
const button = form.button({ name, cb: () => input.click() }).lastItem;
|
||||||
this.label.appendChild(this.button.el);
|
label.appendChild(button.el);
|
||||||
this.label.appendChild(this.input);
|
label.appendChild(input);
|
||||||
this.el.appendChild(this.label);
|
this.el.appendChild(label);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,6 +154,11 @@ export class Form extends Box {
|
||||||
this.el.onsubmit = () => false;
|
this.el.onsubmit = () => false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select(opts) {
|
||||||
|
this.items.push(new Select(opts.name, this, opts));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
button(opts) {
|
button(opts) {
|
||||||
this.items.push(new Button(opts.name, this, opts));
|
this.items.push(new Button(opts.name, this, opts));
|
||||||
return this;
|
return this;
|
||||||
|
@ -154,6 +194,10 @@ export class Form extends Box {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remark(text, opts) {
|
||||||
|
this.document.remark(text, { ...opts, parentEl: this.el });
|
||||||
|
}
|
||||||
|
|
||||||
get lastItem() {
|
get lastItem() {
|
||||||
return this.items[this.items.length - 1];
|
return this.items[this.items.length - 1];
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
import {
|
||||||
|
DEFAULT_OVERLAP_FORCE,
|
||||||
|
DEFAULT_TARGET_RADIUS,
|
||||||
|
DEFAULT_DISTANCE_FACTOR,
|
||||||
|
DEFAULT_OVERLAP_BUFFER,
|
||||||
|
OVERLAP_THRESHOLD_RANDOMIZE,
|
||||||
|
} from '../../util/constants.js';
|
||||||
|
import { Rectangle, Vector } from '../supporting/geometry/index.js';
|
||||||
|
|
||||||
|
const getRectangles = (boxes) => boxes.map((box) => (box instanceof Rectangle ? box : box.rect));
|
||||||
|
const getCenters = (boxes) => getRectangles(boxes).map((rect) => rect.center);
|
||||||
|
|
||||||
|
export const overlapRepulsionForce = (
|
||||||
|
boxA,
|
||||||
|
boxB,
|
||||||
|
force = DEFAULT_OVERLAP_FORCE,
|
||||||
|
margin = DEFAULT_OVERLAP_BUFFER,
|
||||||
|
) => {
|
||||||
|
const [rectA, rectB] = getRectangles([boxA, boxB]);
|
||||||
|
const [centerA, centerB] = getCenters([rectA, rectB]);
|
||||||
|
const r = centerB.subtract(centerA);
|
||||||
|
|
||||||
|
// Apply a stronger force when overlap occurs
|
||||||
|
const overlap = rectA.doesOverlap(rectB);
|
||||||
|
if (overlap) {
|
||||||
|
// If there is sufficient overlap, randomize the direction of force.
|
||||||
|
// Note that we don't want to keep randomizing it once we've picked a direction
|
||||||
|
if (overlap <= OVERLAP_THRESHOLD_RANDOMIZE) {
|
||||||
|
if (!boxB.overlapForceDirection) {
|
||||||
|
boxB.overlapForceDirection = Vector.randomUnitVector(rectB.dim);
|
||||||
|
}
|
||||||
|
return boxB.overlapForceDirection.scale(force);
|
||||||
|
}
|
||||||
|
return r.normalize().scale(force);
|
||||||
|
}
|
||||||
|
boxB.overlapForceDirection = null;
|
||||||
|
|
||||||
|
// Apply a weaker force until distance > margin
|
||||||
|
const separation = rectA.separationFromRect(rectB);
|
||||||
|
if (separation < margin) {
|
||||||
|
return r.normalize().scale(force * ((margin - separation) / margin));
|
||||||
|
}
|
||||||
|
// Otherwise, zero force
|
||||||
|
return Vector.zeros(rectA.dim);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const targetRadiusForce = (
|
||||||
|
boxA,
|
||||||
|
boxB,
|
||||||
|
targetRadius = DEFAULT_TARGET_RADIUS,
|
||||||
|
distanceFactor = DEFAULT_DISTANCE_FACTOR,
|
||||||
|
) => {
|
||||||
|
const [rectA, rectB] = getRectangles([boxA, boxB]);
|
||||||
|
const [centerA, centerB] = getCenters([rectA, rectB]);
|
||||||
|
const r = centerB.subtract(centerA);
|
||||||
|
// Use the distance between the outer edges of the boxes.
|
||||||
|
const outerA = rectA.lineIntersect(centerB, r.scale(-1));
|
||||||
|
const outerB = rectB.lineIntersect(centerA, r);
|
||||||
|
const distance = outerB.subtract(outerA).magnitude;
|
||||||
|
|
||||||
|
// Repel if closer than targetRadius
|
||||||
|
// Attract if farther than targetRadius
|
||||||
|
const force = -distanceFactor * (distance - targetRadius);
|
||||||
|
return r.normalize().scale(force);
|
||||||
|
};
|
|
@ -12,7 +12,7 @@ export class Scene {
|
||||||
constructor(name, rootBox) {
|
constructor(name, rootBox) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.box = rootBox.addBox(name);
|
this.box = rootBox.addBox(name);
|
||||||
this.titleBox = this.box.addBox('Title').setInnerHTML(name);
|
// this.titleBox = this.box.addBox('Title').setInnerHTML(name);
|
||||||
this.box.addBox('Spacer').setInnerHTML(' ');
|
this.box.addBox('Spacer').setInnerHTML(' ');
|
||||||
this.topSection = this.box.addBox('Top section').flex();
|
this.topSection = this.box.addBox('Top section').flex();
|
||||||
this.displayValuesBox = this.topSection.addBox('Values');
|
this.displayValuesBox = this.topSection.addBox('Values');
|
||||||
|
|
|
@ -0,0 +1,164 @@
|
||||||
|
import { Vertex } from '../supporting/vertex.js';
|
||||||
|
import { Edge } from '../supporting/edge.js';
|
||||||
|
import { Document } from './document.js';
|
||||||
|
import { WeightedDirectedGraph } from '../supporting/wdg.js';
|
||||||
|
|
||||||
|
const allGraphs = [];
|
||||||
|
|
||||||
|
const makeWDGHandler = (graphIndex) => (vertexId) => {
|
||||||
|
const graph = allGraphs[graphIndex];
|
||||||
|
// We want a document for editing this node, which may be a vertex or an edge
|
||||||
|
const { editorDoc } = graph;
|
||||||
|
editorDoc.clear();
|
||||||
|
if (vertexId.startsWith('edge:')) {
|
||||||
|
const [, from, to] = vertexId.split(':');
|
||||||
|
Edge.prepareEditorDocument(graph, editorDoc, from, to);
|
||||||
|
} else {
|
||||||
|
Vertex.prepareEditorDocument(graph, editorDoc, vertexId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export class WDGDiagram extends WeightedDirectedGraph {
|
||||||
|
constructor(name, scene, options = {}) {
|
||||||
|
super(name, options);
|
||||||
|
this.scene = scene;
|
||||||
|
this.flowchart = scene?.flowchart;
|
||||||
|
this.editable = options.editable;
|
||||||
|
|
||||||
|
// Mermaid supports a click callback, but we can't customize arguments; we just get the vertex ID.
|
||||||
|
// In order to provide the appropriate graph context for each callback, we create a separate callback
|
||||||
|
// function for each graph.
|
||||||
|
this.index = allGraphs.length;
|
||||||
|
allGraphs.push(this);
|
||||||
|
window[`WDGHandler${this.index}`] = makeWDGHandler(this.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
fromJSON(...args) {
|
||||||
|
super.fromJSON(...args);
|
||||||
|
this.redraw();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addVertex(...args) {
|
||||||
|
const vertex = super.addVertex(...args);
|
||||||
|
vertex.displayVertex();
|
||||||
|
return vertex;
|
||||||
|
}
|
||||||
|
|
||||||
|
addEdge(type, from, to, ...rest) {
|
||||||
|
const existingEdges = this.getEdges(null, from, to);
|
||||||
|
const edge = super.addEdge(type, from, to, ...rest);
|
||||||
|
if (existingEdges.length) {
|
||||||
|
edge.displayEdgeNode();
|
||||||
|
} else {
|
||||||
|
edge.displayEdge();
|
||||||
|
}
|
||||||
|
return edge;
|
||||||
|
}
|
||||||
|
|
||||||
|
setEdgeWeight(...args) {
|
||||||
|
const edge = super.setEdgeWeight(...args);
|
||||||
|
edge.displayEdgeNode();
|
||||||
|
return edge;
|
||||||
|
}
|
||||||
|
|
||||||
|
redraw() {
|
||||||
|
// Call .reset() on all vertices and edges
|
||||||
|
for (const vertex of this.vertices.values()) {
|
||||||
|
vertex.reset();
|
||||||
|
}
|
||||||
|
for (const edges of this.edgeTypes.values()) {
|
||||||
|
for (const edge of edges.values()) {
|
||||||
|
edge.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the target div
|
||||||
|
this.flowchart?.reset();
|
||||||
|
this.flowchart?.init();
|
||||||
|
|
||||||
|
// Draw all vertices and edges
|
||||||
|
for (const vertex of this.vertices.values()) {
|
||||||
|
vertex.displayVertex();
|
||||||
|
}
|
||||||
|
// Let's flatmap and dedupe by [from, to] since each edge
|
||||||
|
// renders all comorphic edges as well.
|
||||||
|
const edgesFrom = new Map(); // edgeMap[from][to] = edge
|
||||||
|
for (const edges of this.edgeTypes.values()) {
|
||||||
|
for (const edge of edges.values()) {
|
||||||
|
const edgesTo = edgesFrom.get(edge.from) || new Map();
|
||||||
|
edgesTo.set(edge.to, edge);
|
||||||
|
edgesFrom.set(edge.from, edgesTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const edgesTo of edgesFrom.values()) {
|
||||||
|
for (const edge of edgesTo.values()) {
|
||||||
|
edge.displayEdge();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure rerender
|
||||||
|
this.flowchart?.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
withFlowchart() {
|
||||||
|
this.scene?.withSectionFlowchart();
|
||||||
|
this.flowchart = this.scene?.lastFlowchart;
|
||||||
|
if (this.editable) {
|
||||||
|
this.controlDoc = new Document('WDGControl', this.flowchart.box.el, { prepend: true });
|
||||||
|
this.editorDoc = new Document('WDGEditor', this.flowchart.box.el);
|
||||||
|
this.errorDoc = new Document('WDGErrors', this.flowchart.box.el);
|
||||||
|
this.resetEditor();
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareControlDoc() {
|
||||||
|
const form = this.controlDoc.form({ name: 'controlForm' }).lastElement;
|
||||||
|
const { subForm: graphPropertiesForm } = form.subForm({ name: 'graphPropsForm' }).lastItem;
|
||||||
|
graphPropertiesForm.flex()
|
||||||
|
.textField({
|
||||||
|
name: 'name',
|
||||||
|
label: 'Graph name',
|
||||||
|
defaultValue: this.name,
|
||||||
|
cb: (({ form: { value: { name } } }) => {
|
||||||
|
this.name = name;
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const { subForm: exportImportForm } = form.subForm({ name: 'exportImportForm' }).lastItem;
|
||||||
|
exportImportForm.flex()
|
||||||
|
.button({
|
||||||
|
name: 'Export',
|
||||||
|
cb: () => {
|
||||||
|
const a = window.document.createElement('a');
|
||||||
|
const json = JSON.stringify(this.toJSON(), null, 2);
|
||||||
|
const currentTime = Math.floor(new Date().getTime() / 1000);
|
||||||
|
a.href = `data:attachment/text,${encodeURI(json)}`;
|
||||||
|
a.target = '_blank';
|
||||||
|
a.download = `wdg_${this.name}_${currentTime}.json`;
|
||||||
|
a.click();
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.fileInput({
|
||||||
|
name: 'Import',
|
||||||
|
cb: ({ input: { files: [file] } }) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = ({ target: { result: text } }) => {
|
||||||
|
console.log('imported file', { file });
|
||||||
|
// this.flowchart?.log(`%% Imported file ${file}`)
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
this.fromJSON(data);
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resetEditor() {
|
||||||
|
this.editorDoc.clear();
|
||||||
|
this.controlDoc.clear();
|
||||||
|
this.prepareControlDoc();
|
||||||
|
Vertex.prepareEditorDocument(this, this.editorDoc);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,56 +1,64 @@
|
||||||
import { ERC721 } from '../supporting/erc721.js';
|
import { ERC1155 } from '../supporting/erc1155.js';
|
||||||
import { randomID } from '../../util/helpers.js';
|
import { randomID } from '../../util/helpers.js';
|
||||||
import { EPSILON } from '../../util/constants.js';
|
import { EPSILON } from '../../util/constants.js';
|
||||||
|
|
||||||
class Lock {
|
class Lock {
|
||||||
constructor(tokenId, amount, duration) {
|
constructor(tokenAddress, tokenTypeId, amount, duration) {
|
||||||
this.dateCreated = new Date();
|
this.dateCreated = new Date();
|
||||||
this.tokenId = tokenId;
|
this.tokenAddress = tokenAddress;
|
||||||
this.amount = amount;
|
this.amount = amount;
|
||||||
this.duration = duration;
|
this.duration = duration;
|
||||||
|
this.tokenTypeId = tokenTypeId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ReputationTokenContract extends ERC721 {
|
export class ReputationTokenContract extends ERC1155 {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('Reputation', 'REP');
|
super('Reputation', 'REP');
|
||||||
this.histories = new Map(); // token id --> {increment, context (i.e. validation pool id)}
|
this.histories = new Map(); // token address --> {tokenTypeId, increment, context (i.e. validation pool id)}
|
||||||
this.values = new Map(); // token id --> current value
|
this.values = new Map(); // token address --> token type id --> current value
|
||||||
this.locks = new Set(); // {tokenId, amount, start, duration}
|
this.locks = new Set(); // {tokenAddress, tokenTypeId, amount, start, duration}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param to
|
* @param to Recipient address
|
||||||
* @param value
|
* @param values Object with reputation type id as key, and amount of reputation as value
|
||||||
* @param context
|
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
mint(to, value, context = {}) {
|
mintBatch(to, tokenTypeIds, values) {
|
||||||
const tokenId = `token_${randomID()}`;
|
const tokenAddress = `token_${randomID()}`;
|
||||||
super.mint(to, tokenId);
|
super.mintBatch(to, tokenAddress, tokenTypeIds, tokenTypeIds.map(() => 1));
|
||||||
this.values.set(tokenId, value);
|
const tokenMap = new Map();
|
||||||
this.histories.set(tokenId, [{ increment: value, context }]);
|
for (let idx = 0; idx < tokenTypeIds.length; idx++) {
|
||||||
return tokenId;
|
const tokenTypeId = tokenTypeIds[idx];
|
||||||
|
const value = values[idx];
|
||||||
|
tokenMap.set(tokenTypeId, value);
|
||||||
|
}
|
||||||
|
this.values.set(tokenAddress, tokenMap);
|
||||||
|
this.histories.set(tokenAddress, [{ operation: 'mintBatch', args: { to, tokenTypeIds, values } }]);
|
||||||
|
return tokenAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
incrementValue(tokenId, increment, context) {
|
incrementValue(tokenAddress, tokenTypeId, increment, context) {
|
||||||
const value = this.values.get(tokenId);
|
const tokenTypeIds = this.values.get(tokenAddress);
|
||||||
if (value === undefined) {
|
if (tokenTypeIds === undefined) {
|
||||||
throw new Error(`Token not found: ${tokenId}`);
|
throw new Error(`Token not found: ${tokenAddress}`);
|
||||||
}
|
}
|
||||||
|
const value = tokenTypeIds?.get(tokenTypeId);
|
||||||
const newValue = value + increment;
|
const newValue = value + increment;
|
||||||
const history = this.histories.get(tokenId) || [];
|
const history = this.histories.get(tokenAddress) || [];
|
||||||
|
|
||||||
if (newValue < -EPSILON) {
|
if (newValue < -EPSILON) {
|
||||||
throw new Error(`Token value can not become negative. Attempted to set value = ${newValue}`);
|
throw new Error(`Token value can not become negative. Attempted to set value = ${newValue}`);
|
||||||
}
|
}
|
||||||
this.values.set(tokenId, newValue);
|
tokenTypeIds.set(tokenAddress, newValue);
|
||||||
history.push({ increment, context });
|
this.values.set(tokenAddress, tokenTypeIds);
|
||||||
this.histories.set(tokenId, history);
|
history.push({ tokenTypeId, increment, context });
|
||||||
|
this.histories.set(tokenAddress, history);
|
||||||
}
|
}
|
||||||
|
|
||||||
transferValueFrom(fromTokenId, toTokenId, amount) {
|
transferValueFrom(from, to, tokenTypeId, amount) {
|
||||||
if (amount === undefined) {
|
if (amount === undefined) {
|
||||||
throw new Error('Transfer value: amount is undefined!');
|
throw new Error('Transfer value: amount is undefined!');
|
||||||
}
|
}
|
||||||
|
@ -60,64 +68,83 @@ export class ReputationTokenContract extends ERC721 {
|
||||||
if (amount < 0) {
|
if (amount < 0) {
|
||||||
throw new Error('Transfer value: amount must be positive');
|
throw new Error('Transfer value: amount must be positive');
|
||||||
}
|
}
|
||||||
const sourceAvailable = this.availableValueOf(fromTokenId);
|
const sourceAvailable = this.availableValueOf(from, tokenTypeId);
|
||||||
if (sourceAvailable < amount - EPSILON) {
|
if (sourceAvailable < amount - EPSILON) {
|
||||||
throw new Error('Token value transfer: source has insufficient available value. '
|
throw new Error('Token value transfer: source has insufficient available value. '
|
||||||
+ `Needs ${amount}; has ${sourceAvailable}.`);
|
+ `Needs ${amount}; has ${sourceAvailable}.`);
|
||||||
}
|
}
|
||||||
this.incrementValue(fromTokenId, -amount);
|
this.incrementValue(from, tokenTypeId, -amount);
|
||||||
this.incrementValue(toTokenId, amount);
|
this.incrementValue(to, tokenTypeId, amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock(tokenId, amount, duration) {
|
batchTransferValueFrom(from, to, tokenTypeIds, amounts) {
|
||||||
const lock = new Lock(tokenId, amount, duration);
|
for (let idx = 0; idx < tokenTypeIds.length; idx++) {
|
||||||
|
const tokenTypeId = tokenTypeIds[idx];
|
||||||
|
const amount = amounts[idx];
|
||||||
|
if (amount === undefined) {
|
||||||
|
throw new Error('Transfer value: amount is undefined!');
|
||||||
|
}
|
||||||
|
if (amount === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (amount < 0) {
|
||||||
|
throw new Error('Transfer value: amount must be positive');
|
||||||
|
}
|
||||||
|
const sourceAvailable = this.availableValueOf(from, tokenTypeId);
|
||||||
|
if (sourceAvailable < amount - EPSILON) {
|
||||||
|
throw new Error('Token value transfer: source has insufficient available value. '
|
||||||
|
+ `Needs ${amount}; has ${sourceAvailable}.`);
|
||||||
|
}
|
||||||
|
this.incrementValue(from, tokenTypeId, -amount);
|
||||||
|
this.incrementValue(to, tokenTypeId, amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lock(tokenAddress, tokenTypeId, amount, duration) {
|
||||||
|
const lock = new Lock(tokenAddress, tokenTypeId, amount, duration);
|
||||||
this.locks.add(lock);
|
this.locks.add(lock);
|
||||||
}
|
}
|
||||||
|
|
||||||
historyOf(tokenId) {
|
historyOf(tokenAddress) {
|
||||||
return this.histories.get(tokenId);
|
return this.histories.get(tokenAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
valueOf(tokenId) {
|
valueOf(tokenAddress, tokenTypeId) {
|
||||||
const value = this.values.get(tokenId);
|
const tokenTypeIds = this.values.get(tokenAddress);
|
||||||
if (value === undefined) {
|
if (tokenTypeIds === undefined) {
|
||||||
throw new Error(`Token not found: ${tokenId}`);
|
throw new Error(`Token not found: ${tokenAddress}`);
|
||||||
}
|
}
|
||||||
return value;
|
return tokenTypeIds.get(tokenTypeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
availableValueOf(tokenId) {
|
availableValueOf(tokenAddress, tokenTypeId) {
|
||||||
const amountLocked = Array.from(this.locks.values())
|
const amountLocked = Array.from(this.locks.values())
|
||||||
.filter(({ tokenId: lockTokenId }) => lockTokenId === tokenId)
|
.filter((lock) => lock.tokenAddress === tokenAddress && lock.tokenTypeId === tokenTypeId)
|
||||||
.filter(({ dateCreated, duration }) => new Date() - dateCreated < duration)
|
.filter(({ dateCreated, duration }) => new Date() - dateCreated < duration)
|
||||||
.reduce((total, { amount }) => total += amount, 0);
|
.reduce((total, { amount }) => total += amount, 0);
|
||||||
|
|
||||||
return this.valueOf(tokenId) - amountLocked;
|
return this.valueOf(tokenAddress, tokenTypeId) - amountLocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
valueOwnedBy(ownerId) {
|
valueOwnedBy(ownerAddress, tokenTypeId) {
|
||||||
return Array.from(this.owners.entries())
|
return Array.from(this.owners.entries())
|
||||||
.filter(([__, owner]) => owner === ownerId)
|
.filter(([__, owner]) => owner === ownerAddress)
|
||||||
.map(([tokenId, __]) => this.valueOf(tokenId))
|
.map(([tokenAddress, __]) => this.valueOf(tokenAddress, tokenTypeId))
|
||||||
.reduce((total, value) => total += value, 0);
|
.reduce((total, value) => total += value, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
availableValueOwnedBy(ownerId) {
|
availableValueOwnedBy(ownerAddress, tokenTypeId) {
|
||||||
return Array.from(this.owners.entries())
|
return Array.from(this.owners.entries())
|
||||||
.filter(([__, owner]) => owner === ownerId)
|
.filter(([__, owner]) => owner === ownerAddress)
|
||||||
.map(([tokenId, __]) => this.availableValueOf(tokenId))
|
.map(([tokenAddress, __]) => this.availableValueOf(tokenAddress, tokenTypeId))
|
||||||
.reduce((total, value) => total += value, 0);
|
.reduce((total, value) => total += value, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTotal() {
|
getTotal(tokenTypeId) {
|
||||||
return Array.from(this.values.values()).reduce((total, value) => total += value, 0);
|
return Array.from(this.values.values())
|
||||||
|
.flatMap((tokens) => tokens.get(tokenTypeId))
|
||||||
|
.reduce((total, value) => total += value, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTotalAvailable() {
|
// burn(tokenAddress, tokenTypeId, )
|
||||||
const amountLocked = Array.from(this.locks.values())
|
|
||||||
.filter(({ dateCreated, duration }) => new Date() - dateCreated < duration)
|
|
||||||
.reduce((total, { amount }) => total += amount, 0);
|
|
||||||
|
|
||||||
return this.getTotal() - amountLocked;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ export class Edge {
|
||||||
return this.graph.getEdges(null, this.from, this.to);
|
return this.graph.getEdges(null, this.from, this.to);
|
||||||
}
|
}
|
||||||
|
|
||||||
getHtml() {
|
getHTML() {
|
||||||
const edges = this.getComorphicEdges();
|
const edges = this.getComorphicEdges();
|
||||||
let html = '';
|
let html = '';
|
||||||
html += '<table>';
|
html += '<table>';
|
||||||
|
@ -49,22 +49,18 @@ export class Edge {
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
getFlowchartNode() {
|
|
||||||
return `${Edge.getCombinedKey(this)}("${this.getHtml()}")`;
|
|
||||||
}
|
|
||||||
|
|
||||||
displayEdgeNode() {
|
displayEdgeNode() {
|
||||||
if (this.options.hide) {
|
if (this.options.hide) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.graph.flowchart?.log(this.getFlowchartNode());
|
this.graph.flowchart?.log(this.getHTML());
|
||||||
}
|
}
|
||||||
|
|
||||||
displayEdge() {
|
displayEdge() {
|
||||||
if (this.options.hide) {
|
if (this.options.hide) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.graph.flowchart?.log(`${this.from.id} --- ${this.getFlowchartNode()} --> ${this.to.id}`);
|
this.graph.flowchart?.log(`${this.from.id} --- ${this.getHTML()} --> ${this.to.id}`);
|
||||||
this.graph.flowchart?.log(`class ${Edge.getCombinedKey(this)} edge`);
|
this.graph.flowchart?.log(`class ${Edge.getCombinedKey(this)} edge`);
|
||||||
if (this.graph.editable && !this.installedClickCallback) {
|
if (this.graph.editable && !this.installedClickCallback) {
|
||||||
this.graph.flowchart?.log(`click ${Edge.getCombinedKey(this)} WDGHandler${this.graph.index} \
|
this.graph.flowchart?.log(`click ${Edge.getCombinedKey(this)} WDGHandler${this.graph.index} \
|
||||||
|
@ -75,6 +71,12 @@ export class Edge {
|
||||||
|
|
||||||
static prepareEditorDocument(graph, doc, from, to) {
|
static prepareEditorDocument(graph, doc, from, to) {
|
||||||
const form = doc.form({ name: 'editorForm' }).lastElement;
|
const form = doc.form({ name: 'editorForm' }).lastElement;
|
||||||
|
form.button({
|
||||||
|
name: 'New Vertex',
|
||||||
|
cb: () => {
|
||||||
|
graph.resetEditor();
|
||||||
|
},
|
||||||
|
});
|
||||||
doc.remark('<h3>Edit Edge</h3>', { parentEl: form.el });
|
doc.remark('<h3>Edit Edge</h3>', { parentEl: form.el });
|
||||||
form
|
form
|
||||||
.textField({
|
.textField({
|
||||||
|
@ -88,7 +90,7 @@ export class Edge {
|
||||||
const subFormArray = form.subFormArray({ id: 'edges', name: 'edges' }).lastItem;
|
const subFormArray = form.subFormArray({ id: 'edges', name: 'edges' }).lastItem;
|
||||||
|
|
||||||
const addEdgeForm = (edge) => {
|
const addEdgeForm = (edge) => {
|
||||||
const { subForm } = form.subForm({ name: 'subform', subFormArray }).lastItem;
|
const { subForm } = form.subForm({ subFormArray }).lastItem;
|
||||||
subForm.textField({
|
subForm.textField({
|
||||||
id: 'type', name: 'type', defaultValue: edge.type, required: true,
|
id: 'type', name: 'type', defaultValue: edge.type, required: true,
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
/**
|
||||||
|
* ERC-1155: Multi Token Standard
|
||||||
|
* See https://eips.ethereum.org/EIPS/eip-1155
|
||||||
|
* and https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC1155/ERC1155.sol
|
||||||
|
*
|
||||||
|
* This implementation is currently incomplete. It lacks the following:
|
||||||
|
* - Token approvals
|
||||||
|
* - Operator approvals
|
||||||
|
* - Emitting events
|
||||||
|
* - transferFrom
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class ERC1155 {
|
||||||
|
constructor(name, symbol) {
|
||||||
|
this.name = name;
|
||||||
|
this.symbol = symbol;
|
||||||
|
this.balances = new Map(); // owner address --> token id --> token count
|
||||||
|
this.owners = new Map(); // token address --> owner address
|
||||||
|
// this.tokenApprovals = new Map(); // token address --> approved addresses
|
||||||
|
// this.operatorApprovals = new Map(); // ownerAddress --> operator approvals
|
||||||
|
|
||||||
|
this.events = {
|
||||||
|
// Transfer: (_from, _to, _tokenAddress) => {},
|
||||||
|
// Approval: (_owner, _approved, _tokenAddress) => {},
|
||||||
|
// ApprovalForAll: (_owner, _operator, _approved) => {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
incrementBalance(ownerAddress, tokenTypeId, increment) {
|
||||||
|
const tokens = this.balances.get(ownerAddress) ?? new Map();
|
||||||
|
const balance = tokens.get(tokenTypeId) ?? 0;
|
||||||
|
tokens.set(tokenTypeId, balance + increment);
|
||||||
|
this.balances.set(ownerAddress, tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
mintBatch(to, tokenAddress, tokenTypeIds, amounts = null) {
|
||||||
|
if (!amounts) {
|
||||||
|
amounts = tokenTypeIds.map(() => 1);
|
||||||
|
}
|
||||||
|
console.log('ERC1155.mintBatch', {
|
||||||
|
to, tokenAddress, tokenTypeIds, amounts,
|
||||||
|
});
|
||||||
|
if (this.owners.get(tokenAddress)) {
|
||||||
|
throw new Error('ERC1155: token already minted');
|
||||||
|
}
|
||||||
|
for (let idx = 0; idx < tokenTypeIds.length; idx++) {
|
||||||
|
const tokenTypeId = tokenTypeIds[idx];
|
||||||
|
const amount = amounts[idx];
|
||||||
|
this.incrementBalance(to, tokenTypeId, amount);
|
||||||
|
}
|
||||||
|
this.owners.set(tokenAddress, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
burn(tokenAddress, tokenTypeId, amount = 1) {
|
||||||
|
const ownerAddress = this.owners.get(tokenAddress);
|
||||||
|
this.incrementBalance(ownerAddress, tokenTypeId, -amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
balanceOf(ownerAddress, tokenTypeId) {
|
||||||
|
if (!ownerAddress) {
|
||||||
|
throw new Error('ERC1155: address zero is not a valid owner');
|
||||||
|
}
|
||||||
|
const tokens = this.balances.get(ownerAddress) ?? new Map();
|
||||||
|
return tokens.get(tokenTypeId) ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ownerOf(tokenAddress) {
|
||||||
|
const ownerAddress = this.owners.get(tokenAddress);
|
||||||
|
if (!ownerAddress) {
|
||||||
|
throw new Error(`ERC1155: invalid token address: ${tokenAddress}`);
|
||||||
|
}
|
||||||
|
return ownerAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
transfer(from, to, tokenAddress) {
|
||||||
|
console.log('ERC1155.transfer', { from, to, tokenAddress });
|
||||||
|
const ownerAddress = this.owners.get(tokenAddress);
|
||||||
|
if (ownerAddress !== from) {
|
||||||
|
throw new Error(`ERC1155: transfer from incorrect owner ${from}; should be ${ownerAddress}`);
|
||||||
|
}
|
||||||
|
this.incrementBalance(from, -1);
|
||||||
|
this.incrementBalance(to, 1);
|
||||||
|
this.owners.set(tokenAddress, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Enable or disable approval for a third party ("operator") to manage
|
||||||
|
/// all of `msg.sender`'s assets
|
||||||
|
/// @dev Emits the ApprovalForAll event. The contract MUST allow
|
||||||
|
/// multiple operators per ownerAddress.
|
||||||
|
/// @param _operator Address to add to the set of authorized operators
|
||||||
|
/// @param _approved True if the operator is approved, false to revoke approval
|
||||||
|
// setApprovalForAll(_operator, _approved) {}
|
||||||
|
|
||||||
|
/// @notice Get the approved address for a single NFT
|
||||||
|
/// @dev Throws if `_tokenAddress` is not a valid NFT.
|
||||||
|
/// @param _tokenAddress The NFT to find the approved address for
|
||||||
|
/// @return The approved address for this NFT, or the zero address if there is none
|
||||||
|
// getApproved(_tokenAddress) {}
|
||||||
|
|
||||||
|
/// @notice Query if an address is an authorized operator for another address
|
||||||
|
/// @param _owner The address that owns the NFTs
|
||||||
|
/// @param _operator The address that acts on behalf of the ownerAddress
|
||||||
|
/// @return True if `_operator` is an approved operator for `_owner`, false otherwise
|
||||||
|
// isApprovedForAll(_owner, _operator) {}
|
||||||
|
}
|
|
@ -16,78 +16,78 @@ export class ERC721 {
|
||||||
this.balances = new Map(); // owner address --> token count
|
this.balances = new Map(); // owner address --> token count
|
||||||
this.owners = new Map(); // token id --> owner address
|
this.owners = new Map(); // token id --> owner address
|
||||||
// this.tokenApprovals = new Map(); // token id --> approved addresses
|
// this.tokenApprovals = new Map(); // token id --> approved addresses
|
||||||
// this.operatorApprovals = new Map(); // owner --> operator approvals
|
// this.operatorApprovals = new Map(); // ownerAddress --> operator approvals
|
||||||
|
|
||||||
this.events = {
|
this.events = {
|
||||||
// Transfer: (_from, _to, _tokenId) => {},
|
// Transfer: (_from, _to, _tokenAddress) => {},
|
||||||
// Approval: (_owner, _approved, _tokenId) => {},
|
// Approval: (_owner, _approved, _tokenAddress) => {},
|
||||||
// ApprovalForAll: (_owner, _operator, _approved) => {},
|
// ApprovalForAll: (_owner, _operator, _approved) => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
incrementBalance(owner, increment) {
|
incrementBalance(ownerAddress, increment) {
|
||||||
const balance = this.balances.get(owner) ?? 0;
|
const balance = this.balances.get(ownerAddress) ?? 0;
|
||||||
this.balances.set(owner, balance + increment);
|
this.balances.set(ownerAddress, balance + increment);
|
||||||
}
|
}
|
||||||
|
|
||||||
mint(to, tokenId) {
|
mint(to, tokenAddress) {
|
||||||
console.log('ERC721.mint', { to, tokenId });
|
console.log('ERC721.mint', { to, tokenAddress });
|
||||||
if (this.owners.get(tokenId)) {
|
if (this.owners.get(tokenAddress)) {
|
||||||
throw new Error('ERC721: token already minted');
|
throw new Error('ERC721: token already minted');
|
||||||
}
|
}
|
||||||
this.incrementBalance(to, 1);
|
this.incrementBalance(to, 1);
|
||||||
this.owners.set(tokenId, to);
|
this.owners.set(tokenAddress, to);
|
||||||
}
|
}
|
||||||
|
|
||||||
burn(tokenId) {
|
burn(tokenAddress) {
|
||||||
const owner = this.owners.get(tokenId);
|
const ownerAddress = this.owners.get(tokenAddress);
|
||||||
this.incrementBalance(owner, -1);
|
this.incrementBalance(ownerAddress, -1);
|
||||||
this.owners.delete(tokenId);
|
this.owners.delete(tokenAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
balanceOf(owner) {
|
balanceOf(ownerAddress) {
|
||||||
if (!owner) {
|
if (!ownerAddress) {
|
||||||
throw new Error('ERC721: address zero is not a valid owner');
|
throw new Error('ERC721: address zero is not a valid owner');
|
||||||
}
|
}
|
||||||
return this.balances.get(owner) ?? 0;
|
return this.balances.get(ownerAddress) ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ownerOf(tokenId) {
|
ownerOf(tokenAddress) {
|
||||||
const owner = this.owners.get(tokenId);
|
const ownerAddress = this.owners.get(tokenAddress);
|
||||||
if (!owner) {
|
if (!ownerAddress) {
|
||||||
throw new Error(`ERC721: invalid token ID: ${tokenId}`);
|
throw new Error(`ERC721: invalid token ID: ${tokenAddress}`);
|
||||||
}
|
}
|
||||||
return owner;
|
return ownerAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
transfer(from, to, tokenId) {
|
transfer(from, to, tokenAddress) {
|
||||||
console.log('ERC721.transfer', { from, to, tokenId });
|
console.log('ERC721.transfer', { from, to, tokenAddress });
|
||||||
const owner = this.owners.get(tokenId);
|
const ownerAddress = this.owners.get(tokenAddress);
|
||||||
if (owner !== from) {
|
if (ownerAddress !== from) {
|
||||||
throw new Error(`ERC721: transfer from incorrect owner ${from}; should be ${owner}`);
|
throw new Error(`ERC721: transfer from incorrect owner ${from}; should be ${ownerAddress}`);
|
||||||
}
|
}
|
||||||
this.incrementBalance(from, -1);
|
this.incrementBalance(from, -1);
|
||||||
this.incrementBalance(to, 1);
|
this.incrementBalance(to, 1);
|
||||||
this.owners.set(tokenId, to);
|
this.owners.set(tokenAddress, to);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Enable or disable approval for a third party ("operator") to manage
|
/// @notice Enable or disable approval for a third party ("operator") to manage
|
||||||
/// all of `msg.sender`'s assets
|
/// all of `msg.sender`'s assets
|
||||||
/// @dev Emits the ApprovalForAll event. The contract MUST allow
|
/// @dev Emits the ApprovalForAll event. The contract MUST allow
|
||||||
/// multiple operators per owner.
|
/// multiple operators per ownerAddress.
|
||||||
/// @param _operator Address to add to the set of authorized operators
|
/// @param _operator Address to add to the set of authorized operators
|
||||||
/// @param _approved True if the operator is approved, false to revoke approval
|
/// @param _approved True if the operator is approved, false to revoke approval
|
||||||
// setApprovalForAll(_operator, _approved) {}
|
// setApprovalForAll(_operator, _approved) {}
|
||||||
|
|
||||||
/// @notice Get the approved address for a single NFT
|
/// @notice Get the approved address for a single NFT
|
||||||
/// @dev Throws if `_tokenId` is not a valid NFT.
|
/// @dev Throws if `_tokenAddress` is not a valid NFT.
|
||||||
/// @param _tokenId The NFT to find the approved address for
|
/// @param _tokenAddress The NFT to find the approved address for
|
||||||
/// @return The approved address for this NFT, or the zero address if there is none
|
/// @return The approved address for this NFT, or the zero address if there is none
|
||||||
// getApproved(_tokenId) {}
|
// getApproved(_tokenAddress) {}
|
||||||
|
|
||||||
/// @notice Query if an address is an authorized operator for another address
|
/// @notice Query if an address is an authorized operator for another address
|
||||||
/// @param _owner The address that owns the NFTs
|
/// @param _owner The address that owns the NFTs
|
||||||
/// @param _operator The address that acts on behalf of the owner
|
/// @param _operator The address that acts on behalf of the ownerAddress
|
||||||
/// @return True if `_operator` is an approved operator for `_owner`, false otherwise
|
/// @return True if `_operator` is an approved operator for `_owner`, false otherwise
|
||||||
// isApprovedForAll(_owner, _operator) {}
|
// isApprovedForAll(_owner, _operator) {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './vector.js';
|
||||||
|
export * from './polygon.js';
|
||||||
|
export * from './rectangle.js';
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Vector } from './vector.js';
|
||||||
|
|
||||||
|
export class Polygon {
|
||||||
|
constructor() {
|
||||||
|
this.vertices = [];
|
||||||
|
this.dim = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
addVertex(point) {
|
||||||
|
point = point instanceof Vector ? point : Vector.from(point);
|
||||||
|
if (!this.dim) {
|
||||||
|
this.dim = point.dim;
|
||||||
|
} else if (this.dim !== point.dim) {
|
||||||
|
throw new Error('All vertices of a polygon must have the same dimensionality');
|
||||||
|
}
|
||||||
|
this.vertices.push(point);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
import { DEFAULT_OVERLAP_BUFFER } from '../../../util/constants.js';
|
||||||
|
import { Polygon } from './polygon.js';
|
||||||
|
import { Vector } from './vector.js';
|
||||||
|
|
||||||
|
export class Rectangle extends Polygon {
|
||||||
|
constructor(position, dimensions) {
|
||||||
|
super();
|
||||||
|
if (this.vertices.length) {
|
||||||
|
throw new Error('Reinitializing geometry is not allowed');
|
||||||
|
}
|
||||||
|
this.position = Vector.from(position);
|
||||||
|
this.dimensions = Vector.from(dimensions);
|
||||||
|
// Next point is obtained by moving the specified length along each dimension
|
||||||
|
// one at a time, then reversing these movements in the same order.
|
||||||
|
let point = Vector.from(this.position);
|
||||||
|
for (let dim = 0; dim < point.dim; dim++) {
|
||||||
|
this.addVertex(point);
|
||||||
|
const increment = Vector.unitVector(dim, point.dim).scale(this.dimensions[dim]);
|
||||||
|
point = point.add(increment);
|
||||||
|
}
|
||||||
|
for (let dim = 0; dim < point.dim; dim++) {
|
||||||
|
this.addVertex(point);
|
||||||
|
const increment = Vector.unitVector(dim, point.dim).scale(this.dimensions[dim]);
|
||||||
|
point = point.subtract(increment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get center() {
|
||||||
|
return Vector.from(this.dimensions.map((Q, idx) => this.position[idx] + Q / 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
doesOverlap(rect) {
|
||||||
|
const overlapFractions = this.dimensions.map((_, dim) => {
|
||||||
|
const thisMin = this.position[dim];
|
||||||
|
const thisMax = this.position[dim] + this.dimensions[dim];
|
||||||
|
const thatMin = rect.position[dim];
|
||||||
|
const thatMax = rect.position[dim] + rect.dimensions[dim];
|
||||||
|
if (thatMin <= thisMin && thatMax >= thisMin) {
|
||||||
|
if (thatMax >= thisMax) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return (thatMax - thisMin) / (thatMax - thatMin);
|
||||||
|
}
|
||||||
|
if (thatMin <= thisMax && thatMax >= thisMin) {
|
||||||
|
if (thatMax <= thisMax) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return (thisMax - thatMin) / (thatMax - thatMin);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
if (overlapFractions.every((x) => x > 0)) {
|
||||||
|
return Math.max(...overlapFractions);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
doesContain(point) {
|
||||||
|
return this.dimensions.every((_, dim) => {
|
||||||
|
const thisMin = this.position[dim];
|
||||||
|
const thisMax = this.position[dim] + this.dimensions[dim];
|
||||||
|
return point[dim] >= thisMin && point[dim] <= thisMax;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get aspectRatio() {
|
||||||
|
const [width, height] = this.dimensions;
|
||||||
|
return height / width;
|
||||||
|
}
|
||||||
|
|
||||||
|
lineIntersect(startPoint, direction) {
|
||||||
|
const r = Vector.from(direction).normalize();
|
||||||
|
let point = Vector.from(startPoint);
|
||||||
|
const maxDistance = this.center.subtract(point).magnitude;
|
||||||
|
let increment = maxDistance;
|
||||||
|
let everInside = false;
|
||||||
|
for (let i = 0; i < 15; i++) {
|
||||||
|
if (this.doesContain(point)) {
|
||||||
|
everInside = true;
|
||||||
|
point = point.subtract(r.scale(increment));
|
||||||
|
} else {
|
||||||
|
point = point.add(r.scale(increment));
|
||||||
|
}
|
||||||
|
increment /= 2;
|
||||||
|
}
|
||||||
|
return everInside ? point : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
addMargin(margin = DEFAULT_OVERLAP_BUFFER) {
|
||||||
|
const position = this.position.subtract([margin, margin]);
|
||||||
|
const dimensions = this.dimensions.add([2 * margin, 2 * margin]);
|
||||||
|
return new Rectangle(position, dimensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
separationFromRect(rect) {
|
||||||
|
if (this.doesOverlap(rect)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const r = rect.center.subtract(this.center);
|
||||||
|
const outerA = this.lineIntersect(rect.center, r.scale(-1));
|
||||||
|
const outerB = rect.lineIntersect(this.center, r);
|
||||||
|
return outerA.subtract(outerB).magnitude;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
export class Vector extends Array {
|
||||||
|
get dim() {
|
||||||
|
return this.length ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
add(vector) {
|
||||||
|
vector = vector instanceof Vector ? vector : Vector.from(vector);
|
||||||
|
if (vector.dim !== this.dim) {
|
||||||
|
throw new Error('Can only add vectors of the same dimensions');
|
||||||
|
}
|
||||||
|
return Vector.from(this.map((q, idx) => q + vector[idx]));
|
||||||
|
}
|
||||||
|
|
||||||
|
subtract(vector) {
|
||||||
|
vector = vector instanceof Vector ? vector : Vector.from(vector);
|
||||||
|
if (vector.dim !== this.dim) {
|
||||||
|
throw new Error('Can only subtract vectors of the same dimensions');
|
||||||
|
}
|
||||||
|
return Vector.from(this.map((q, idx) => q - vector[idx]));
|
||||||
|
}
|
||||||
|
|
||||||
|
static unitVector(dim, totalDim) {
|
||||||
|
return Vector.from(Array(totalDim), (_, idx) => (idx === dim ? 1 : 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
get magnitudeSquared() {
|
||||||
|
return this.reduce((total, q) => total += q ** 2, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
get magnitude() {
|
||||||
|
return Math.sqrt(this.magnitudeSquared);
|
||||||
|
}
|
||||||
|
|
||||||
|
scale(factor) {
|
||||||
|
return Vector.from(this.map((q) => q * factor));
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize() {
|
||||||
|
return this.scale(1 / this.magnitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
static randomUnitVector(totalDim) {
|
||||||
|
return Vector.from(Array(totalDim), () => Math.random() - 0.5).normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
static zeros(totalDim) {
|
||||||
|
return Vector.from(Array(totalDim), () => 0);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
import { Document } from '../display/document.js';
|
||||||
|
|
||||||
|
export const PropertyTypes = {
|
||||||
|
string: 'string',
|
||||||
|
number: 'number',
|
||||||
|
};
|
||||||
|
|
||||||
|
class Property {
|
||||||
|
constructor(name, type) {
|
||||||
|
this.name = name;
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NodeType {
|
||||||
|
constructor(name) {
|
||||||
|
this.name = name;
|
||||||
|
this.properties = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
addProperty(name, type) {
|
||||||
|
this.properties.add(new Property(name, type));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EdgeType {
|
||||||
|
constructor(name, fromNodeTypes, toNodeTypes) {
|
||||||
|
this.name = name;
|
||||||
|
this.fromNodeTypes = fromNodeTypes;
|
||||||
|
this.toNodeTypes = toNodeTypes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Schema {
|
||||||
|
constructor() {
|
||||||
|
this.nodeTypes = new Set();
|
||||||
|
this.edgeTypes = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
addNodeType(name) {
|
||||||
|
const nodeType = new NodeType(name);
|
||||||
|
this.nodeTypes.add(nodeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
addEdgeType(name) {
|
||||||
|
const edgeType = new EdgeType(name);
|
||||||
|
this.nodeTypes.add(edgeType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SchemaEditor {
|
||||||
|
constructor(name, scene, options = {}) {
|
||||||
|
this.name = name;
|
||||||
|
this.scene = scene;
|
||||||
|
this.options = options;
|
||||||
|
|
||||||
|
this.schemaEditorBox = scene.middleSection.addBox('Schema Editor').flex();
|
||||||
|
this.nodeEditorDoc = new Document('NodeSchemaEditor', this.schemaEditorBox.el);
|
||||||
|
this.edgeEditorDoc = new Document('EdgeSchemaEditor', this.schemaEditorBox.el);
|
||||||
|
this.prepareNodeEditorDocument();
|
||||||
|
this.prepareEdgeEditorDocument();
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareNodeEditorDocument(schema = new Schema()) {
|
||||||
|
const doc = this.nodeEditorDoc;
|
||||||
|
doc.clear();
|
||||||
|
doc.remark('<h2>Node Types</h2>');
|
||||||
|
const form = doc.form({ name: 'Node Types Editor' }).lastElement;
|
||||||
|
const nodeTypesSubFormArray = form.subFormArray({ name: 'nodeTypes' }).lastItem;
|
||||||
|
|
||||||
|
const addNodeForm = (name, properties) => {
|
||||||
|
const nodeTypeForm = nodeTypesSubFormArray.subForm().lastSubForm;
|
||||||
|
if (name) {
|
||||||
|
nodeTypeForm.remark(`<h3>Node Type: ${name}</h3>`);
|
||||||
|
} else {
|
||||||
|
nodeTypeForm.remark('<h3>New Node Type</h3>');
|
||||||
|
}
|
||||||
|
nodeTypeForm.textField({ name: 'name', defaultValue: name });
|
||||||
|
const propertiesSubFormArray = nodeTypeForm.subFormArray({ name: 'properties' }).lastItem;
|
||||||
|
for (const property of properties.values()) {
|
||||||
|
const propertyForm = propertiesSubFormArray.subForm().lastSubForm;
|
||||||
|
propertyForm.textField({ name: 'name', defaultValue: property.name });
|
||||||
|
propertyForm.textField({ name: 'type', defaultValue: property.type });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const { name, properties } of schema.nodeTypes.values()) {
|
||||||
|
addNodeForm(name, properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
form.button({
|
||||||
|
name: 'Add Node Type',
|
||||||
|
cb: () => {
|
||||||
|
addNodeForm('', new Set());
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
form.submit({
|
||||||
|
name: 'Save',
|
||||||
|
cb: ({ form: { value: formValue } }) => {
|
||||||
|
console.log('save', { formValue });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareEdgeEditorDocument(schema = new Schema()) {
|
||||||
|
const doc = this.edgeEditorDoc;
|
||||||
|
doc.clear();
|
||||||
|
doc.remark('<h2>Edge Types</h2>');
|
||||||
|
const form = doc.form('Edge Types Editor').lastElement;
|
||||||
|
for (const { name } of schema.edgeTypes.values()) {
|
||||||
|
form.remark(`<h3>Edge Type: ${name}</h3>`);
|
||||||
|
form.textField({ name: 'name', defaultValue: name });
|
||||||
|
}
|
||||||
|
form.submit({
|
||||||
|
name: 'Save',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Properties
|
||||||
|
// Data types
|
||||||
|
// Relationships
|
|
@ -1,14 +1,15 @@
|
||||||
export class Stake {
|
export class Stake {
|
||||||
constructor({
|
constructor({
|
||||||
tokenId, position, amount, lockingTime,
|
tokenAddress, tokenTypeId, position, amount, lockingTime,
|
||||||
}) {
|
}) {
|
||||||
this.tokenId = tokenId;
|
this.tokenAddress = tokenAddress;
|
||||||
this.position = position;
|
this.position = position;
|
||||||
this.amount = amount;
|
this.amount = amount;
|
||||||
this.lockingTime = lockingTime;
|
this.lockingTime = lockingTime;
|
||||||
|
this.tokenTypeId = tokenTypeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
getStakeValue({ lockingTimeExponent } = {}) {
|
getAmount({ lockingTimeExponent } = {}) {
|
||||||
return this.amount * this.lockingTime ** lockingTimeExponent;
|
return this.amount * this.lockingTime ** lockingTimeExponent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ export class Vertex {
|
||||||
to: [],
|
to: [],
|
||||||
};
|
};
|
||||||
this.installedClickCallback = false;
|
this.installedClickCallback = false;
|
||||||
this.properties = new Map();
|
this.properties = options.properties ?? new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
|
@ -40,29 +40,37 @@ export class Vertex {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setProperty(key, value) {
|
async setProperty(key, value) {
|
||||||
this.properties.set(key, value);
|
this.properties.set(key, value);
|
||||||
|
await this.onUpdate?.();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getHTML() {
|
||||||
|
let html = '';
|
||||||
|
if (this.type) {
|
||||||
|
html += `<span class='small'>${this.type}</span><br>`;
|
||||||
|
}
|
||||||
|
html += `${this.label || this.id}`;
|
||||||
|
html += '<table>';
|
||||||
|
for (const [key, value] of this.properties.entries()) {
|
||||||
|
const displayValue = typeof value === 'number' ? displayNumber(value) : value;
|
||||||
|
html += `<tr><td>${key}</td><td>${displayValue}</td></tr>`;
|
||||||
|
}
|
||||||
|
html += '</table>';
|
||||||
|
if (this.label && this.id !== this.label) {
|
||||||
|
html += `<span class=small>${this.id}</span><br>`;
|
||||||
|
}
|
||||||
|
html = html.replaceAll(/\n\s*/g, '');
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
displayVertex() {
|
displayVertex() {
|
||||||
if (this.options.hide) {
|
if (this.options.hide) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let html = '';
|
this.graph.flowchart?.log(`${this.id}["${this.getHTML()}"]`);
|
||||||
html += `${this.label}`;
|
|
||||||
html += '<table>';
|
|
||||||
for (const [key, value] of this.properties.entries()) {
|
|
||||||
const displayValue = typeof value === 'number' ? displayNumber(value) : value;
|
|
||||||
html += `<tr><td>${key}</td><td>${displayValue}</td></tr>`;
|
|
||||||
}
|
|
||||||
html += '</table>';
|
|
||||||
if (this.id !== this.label) {
|
|
||||||
html += `<span class=small>${this.id}</span><br>`;
|
|
||||||
}
|
|
||||||
html = html.replaceAll(/\n\s*/g, '');
|
|
||||||
this.graph.flowchart?.log(`${this.id}["${html}"]`);
|
|
||||||
|
|
||||||
if (this.graph.editable && !this.installedClickCallback) {
|
if (this.graph.editable && !this.installedClickCallback) {
|
||||||
this.graph.flowchart?.log(`click ${this.id} WDGHandler${this.graph.index} "Edit Vertex ${this.id}"`);
|
this.graph.flowchart?.log(`click ${this.id} WDGHandler${this.graph.index} "Edit Vertex ${this.id}"`);
|
||||||
|
@ -73,26 +81,34 @@ export class Vertex {
|
||||||
static prepareEditorDocument(graph, doc, vertexId) {
|
static prepareEditorDocument(graph, doc, vertexId) {
|
||||||
const vertex = vertexId ? graph.getVertex(vertexId) : undefined;
|
const vertex = vertexId ? graph.getVertex(vertexId) : undefined;
|
||||||
const form = doc.form().lastElement;
|
const form = doc.form().lastElement;
|
||||||
|
|
||||||
|
if (vertex) {
|
||||||
|
form.button({
|
||||||
|
name: 'New Vertex',
|
||||||
|
cb: () => {
|
||||||
|
graph.resetEditor();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
doc.remark(`<h3>${vertex ? 'Edit' : 'Add'} Vertex</h3>`, { parentEl: form.el });
|
doc.remark(`<h3>${vertex ? 'Edit' : 'Add'} Vertex</h3>`, { parentEl: form.el });
|
||||||
form
|
form
|
||||||
.textField({
|
.textField({
|
||||||
id: 'id', name: 'id', defaultValue: vertex?.id,
|
name: 'id', defaultValue: vertex?.id,
|
||||||
})
|
})
|
||||||
.textField({ id: 'type', name: 'type', defaultValue: vertex?.type })
|
.textField({ name: 'type', defaultValue: vertex?.type })
|
||||||
.textField({ id: 'label', name: 'label', defaultValue: vertex?.label });
|
.textField({ name: 'label', defaultValue: vertex?.label });
|
||||||
|
|
||||||
doc.remark('<h4>Properties</h4>', { parentEl: form.el });
|
doc.remark('<h4>Properties</h4>', { parentEl: form.el });
|
||||||
const subFormArray = form.subFormArray({ id: 'properties', name: 'properties' }).lastItem;
|
const subFormArray = form.subFormArray({ name: 'properties' }).lastItem;
|
||||||
const addPropertyForm = (key, value) => {
|
const addPropertyForm = (key, value) => {
|
||||||
const { subForm } = form.subForm({ name: 'subform', subFormArray }).lastItem;
|
const { subForm } = form.subForm({ subFormArray }).lastItem;
|
||||||
subForm.textField({ id: 'key', name: 'key', defaultValue: key })
|
subForm.textField({ name: 'key', defaultValue: key })
|
||||||
.textField({ id: 'value', name: 'value', defaultValue: value })
|
.textField({ name: 'value', defaultValue: value })
|
||||||
.button({
|
.button({
|
||||||
id: 'remove',
|
|
||||||
name: 'Remove Property',
|
name: 'Remove Property',
|
||||||
cb: () => subFormArray.remove(subForm),
|
cb: () => subFormArray.remove(subForm),
|
||||||
});
|
})
|
||||||
doc.remark('<br>', { parentEl: subForm.el });
|
.remark('<br>');
|
||||||
};
|
};
|
||||||
|
|
||||||
if (vertex) {
|
if (vertex) {
|
||||||
|
@ -102,13 +118,11 @@ export class Vertex {
|
||||||
}
|
}
|
||||||
|
|
||||||
form.button({
|
form.button({
|
||||||
id: 'add',
|
|
||||||
name: 'Add Property',
|
name: 'Add Property',
|
||||||
cb: () => addPropertyForm('', ''),
|
cb: () => addPropertyForm('', ''),
|
||||||
});
|
});
|
||||||
|
|
||||||
form.submit({
|
form.submit({
|
||||||
id: 'save',
|
|
||||||
name: 'Save',
|
name: 'Save',
|
||||||
cb: ({ form: { value: formValue } }) => {
|
cb: ({ form: { value: formValue } }) => {
|
||||||
let fullRedraw = false;
|
let fullRedraw = false;
|
||||||
|
@ -121,7 +135,10 @@ export class Vertex {
|
||||||
Object.assign(vertex, formValue);
|
Object.assign(vertex, formValue);
|
||||||
vertex.displayVertex();
|
vertex.displayVertex();
|
||||||
} else {
|
} else {
|
||||||
const newVertex = graph.addVertex(formValue.type, formValue.id, null, formValue.label);
|
const {
|
||||||
|
type, id, label, properties,
|
||||||
|
} = formValue;
|
||||||
|
const newVertex = graph.addVertex(type, id, null, label, { properties });
|
||||||
Object.assign(newVertex, formValue);
|
Object.assign(newVertex, formValue);
|
||||||
doc.clear();
|
doc.clear();
|
||||||
Vertex.prepareEditorDocument(graph, doc, newVertex.id);
|
Vertex.prepareEditorDocument(graph, doc, newVertex.id);
|
||||||
|
@ -134,7 +151,6 @@ export class Vertex {
|
||||||
|
|
||||||
if (vertex) {
|
if (vertex) {
|
||||||
form.button({
|
form.button({
|
||||||
id: 'delete',
|
|
||||||
name: 'Delete Vertex',
|
name: 'Delete Vertex',
|
||||||
cb: () => {
|
cb: () => {
|
||||||
graph.deleteVertex(vertex.id);
|
graph.deleteVertex(vertex.id);
|
||||||
|
@ -159,7 +175,6 @@ export class Vertex {
|
||||||
}
|
}
|
||||||
|
|
||||||
form.button({
|
form.button({
|
||||||
id: 'cancel',
|
|
||||||
name: 'Cancel',
|
name: 'Cancel',
|
||||||
cb: () => graph.resetEditor(),
|
cb: () => graph.resetEditor(),
|
||||||
parentEl: doc.el,
|
parentEl: doc.el,
|
||||||
|
|
|
@ -1,38 +1,12 @@
|
||||||
import { Vertex } from './vertex.js';
|
import { Vertex } from './vertex.js';
|
||||||
import { Edge } from './edge.js';
|
import { Edge } from './edge.js';
|
||||||
import { Document } from '../display/document.js';
|
|
||||||
|
|
||||||
const allGraphs = [];
|
|
||||||
|
|
||||||
const makeWDGHandler = (graphIndex) => (vertexId) => {
|
|
||||||
const graph = allGraphs[graphIndex];
|
|
||||||
// We want a document for editing this node, which may be a vertex or an edge
|
|
||||||
const { editorDoc } = graph;
|
|
||||||
editorDoc.clear();
|
|
||||||
if (vertexId.startsWith('edge:')) {
|
|
||||||
const [, from, to] = vertexId.split(':');
|
|
||||||
Edge.prepareEditorDocument(graph, editorDoc, from, to);
|
|
||||||
} else {
|
|
||||||
Vertex.prepareEditorDocument(graph, editorDoc, vertexId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export class WeightedDirectedGraph {
|
export class WeightedDirectedGraph {
|
||||||
constructor(name, scene, options = {}) {
|
constructor(name) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.scene = scene;
|
|
||||||
this.vertices = new Map();
|
this.vertices = new Map();
|
||||||
this.edgeTypes = new Map();
|
this.edgeTypes = new Map();
|
||||||
this.nextVertexId = 0;
|
this.nextVertexId = 0;
|
||||||
this.flowchart = scene?.flowchart;
|
|
||||||
this.editable = options.editable;
|
|
||||||
|
|
||||||
// Mermaid supports a click callback, but we can't customize arguments; we just get the vertex ID.
|
|
||||||
// In order to provide the appropriate graph context for each callback, we create a separate callback
|
|
||||||
// function for each graph.
|
|
||||||
this.index = allGraphs.length;
|
|
||||||
allGraphs.push(this);
|
|
||||||
window[`WDGHandler${this.index}`] = makeWDGHandler(this.index);
|
|
||||||
|
|
||||||
// TODO: Populate history
|
// TODO: Populate history
|
||||||
this.history = [];
|
this.history = [];
|
||||||
|
@ -72,108 +46,9 @@ export class WeightedDirectedGraph {
|
||||||
} of edges) {
|
} of edges) {
|
||||||
this.addEdge(type, from, to, weight);
|
this.addEdge(type, from, to, weight);
|
||||||
}
|
}
|
||||||
this.redraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
redraw() {
|
|
||||||
// Call .reset() on all vertices and edges
|
|
||||||
for (const vertex of this.vertices.values()) {
|
|
||||||
vertex.reset();
|
|
||||||
}
|
|
||||||
for (const edges of this.edgeTypes.values()) {
|
|
||||||
for (const edge of edges.values()) {
|
|
||||||
edge.reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear the target div
|
|
||||||
this.flowchart?.reset();
|
|
||||||
this.flowchart?.init();
|
|
||||||
|
|
||||||
// Draw all vertices and edges
|
|
||||||
for (const vertex of this.vertices.values()) {
|
|
||||||
vertex.displayVertex();
|
|
||||||
}
|
|
||||||
// Let's flatmap and dedupe by [from, to] since each edge
|
|
||||||
// renders all comorphic edges as well.
|
|
||||||
const edgesFrom = new Map(); // edgeMap[from][to] = edge
|
|
||||||
for (const edges of this.edgeTypes.values()) {
|
|
||||||
for (const edge of edges.values()) {
|
|
||||||
const edgesTo = edgesFrom.get(edge.from) || new Map();
|
|
||||||
edgesTo.set(edge.to, edge);
|
|
||||||
edgesFrom.set(edge.from, edgesTo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const edgesTo of edgesFrom.values()) {
|
|
||||||
for (const edge of edgesTo.values()) {
|
|
||||||
edge.displayEdge();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure rerender
|
|
||||||
this.flowchart?.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
withFlowchart() {
|
|
||||||
this.scene?.withSectionFlowchart();
|
|
||||||
this.flowchart = this.scene?.lastFlowchart;
|
|
||||||
if (this.editable) {
|
|
||||||
this.controlDoc = new Document('WDGControl', this.flowchart.box.el, { prepend: true });
|
|
||||||
this.editorDoc = new Document('WDGEditor', this.flowchart.box.el);
|
|
||||||
this.errorDoc = new Document('WDGErrors', this.flowchart.box.el);
|
|
||||||
this.resetEditor();
|
|
||||||
}
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareControlDoc() {
|
|
||||||
const form = this.controlDoc.form({ name: 'controlForm' }).lastElement;
|
|
||||||
const { subForm: graphPropertiesForm } = form.subForm({ name: 'graphPropsForm' }).lastItem;
|
|
||||||
graphPropertiesForm.flex()
|
|
||||||
.textField({ name: 'name', label: 'Graph name', defaultValue: this.name })
|
|
||||||
.submit({
|
|
||||||
name: 'Save',
|
|
||||||
cb: (({ form: { value: { name } } }) => {
|
|
||||||
this.name = name;
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const { subForm: exportImportForm } = form.subForm({ name: 'exportImportForm' }).lastItem;
|
|
||||||
exportImportForm.flex()
|
|
||||||
.button({
|
|
||||||
name: 'Export',
|
|
||||||
cb: () => {
|
|
||||||
const a = window.document.createElement('a');
|
|
||||||
const json = JSON.stringify(this.toJSON(), null, 2);
|
|
||||||
const currentTime = Math.floor(new Date().getTime() / 1000);
|
|
||||||
a.href = `data:attachment/text,${encodeURI(json)}`;
|
|
||||||
a.target = '_blank';
|
|
||||||
a.download = `wdg_${this.name}_${currentTime}.json`;
|
|
||||||
a.click();
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.fileInput({
|
|
||||||
name: 'Import',
|
|
||||||
cb: ({ input: { files: [file] } }) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = ({ target: { result: text } }) => {
|
|
||||||
console.log('imported file', { file });
|
|
||||||
// this.flowchart?.log(`%% Imported file ${file}`)
|
|
||||||
const data = JSON.parse(text);
|
|
||||||
this.fromJSON(data);
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
resetEditor() {
|
|
||||||
this.editorDoc.clear();
|
|
||||||
this.controlDoc.clear();
|
|
||||||
this.prepareControlDoc();
|
|
||||||
Vertex.prepareEditorDocument(this, this.editorDoc);
|
|
||||||
}
|
|
||||||
|
|
||||||
addVertex(type, id, data, label, options) {
|
addVertex(type, id, data, label, options) {
|
||||||
// Supports simple case of auto-incremented numeric ids
|
// Supports simple case of auto-incremented numeric ids
|
||||||
if (typeof id === 'object') {
|
if (typeof id === 'object') {
|
||||||
|
@ -186,7 +61,6 @@ export class WeightedDirectedGraph {
|
||||||
}
|
}
|
||||||
const vertex = new Vertex(this, type, id, data, { ...options, label });
|
const vertex = new Vertex(this, type, id, data, { ...options, label });
|
||||||
this.vertices.set(id, vertex);
|
this.vertices.set(id, vertex);
|
||||||
vertex.displayVertex();
|
|
||||||
return vertex;
|
return vertex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,7 +103,6 @@ export class WeightedDirectedGraph {
|
||||||
}
|
}
|
||||||
const edgeKey = Edge.getKey(edge);
|
const edgeKey = Edge.getKey(edge);
|
||||||
edges.set(edgeKey, edge);
|
edges.set(edgeKey, edge);
|
||||||
edge.displayEdgeNode();
|
|
||||||
return edge;
|
return edge;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,15 +112,9 @@ export class WeightedDirectedGraph {
|
||||||
if (this.getEdge(type, from, to)) {
|
if (this.getEdge(type, from, to)) {
|
||||||
throw new Error(`Edge ${type} from ${from.id} to ${to.id} already exists`);
|
throw new Error(`Edge ${type} from ${from.id} to ${to.id} already exists`);
|
||||||
}
|
}
|
||||||
const existingEdges = this.getEdges(null, from, to);
|
|
||||||
const edge = this.setEdgeWeight(type, from, to, weight, data, options);
|
const edge = this.setEdgeWeight(type, from, to, weight, data, options);
|
||||||
from.edges.from.push(edge);
|
from.edges.from.push(edge);
|
||||||
to.edges.to.push(edge);
|
to.edges.to.push(edge);
|
||||||
if (existingEdges.length) {
|
|
||||||
edge.displayEdgeNode();
|
|
||||||
} else {
|
|
||||||
edge.displayEdge();
|
|
||||||
}
|
|
||||||
return edge;
|
return edge;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,12 @@ a:visited {
|
||||||
left: 12em;
|
left: 12em;
|
||||||
top: -0.5em;
|
top: -0.5em;
|
||||||
}
|
}
|
||||||
|
.fixed {
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
.absolute {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
svg {
|
svg {
|
||||||
width: 800px;
|
width: 800px;
|
||||||
}
|
}
|
||||||
|
@ -106,4 +112,22 @@ span.small {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
|
}
|
||||||
|
.force-directed-graph {
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
.force-directed-graph > canvas {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.force-directed-graph > .box {
|
||||||
|
border: 1px hsl(195.4545454545, 4%, 39.4117647059%) solid;
|
||||||
|
color: #b6b6b6;
|
||||||
|
text-align: center;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
.force-directed-graph > .vertex {
|
||||||
|
background-color: #216262;
|
||||||
|
}
|
||||||
|
.force-directed-graph > .edge {
|
||||||
|
background-color: #2a5b6c;
|
||||||
}
|
}
|
|
@ -15,18 +15,19 @@
|
||||||
For more information please see the <a href="https://daogovernanceframework.com/wiki/DAO_Governance_Framework">DGF
|
For more information please see the <a href="https://daogovernanceframework.com/wiki/DAO_Governance_Framework">DGF
|
||||||
Wiki</a>.
|
Wiki</a>.
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
The code for this site is available in <a
|
||||||
|
href="https://gitlab.com/dao-governance-framework/science-publishing-dao/-/tree/main/forum-network/src">GitLab</a>.
|
||||||
|
</p>
|
||||||
<h2>Tools</h2>
|
<h2>Tools</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="./wdg-editor">Weighted Directed Graph Editor</a></li>
|
<li><a href="./wdg-editor">Weighted Directed Graph Editor</a></li>
|
||||||
|
<li><a href="./schema-editor">Schema Editor</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Example Scenarios</h2>
|
<h2>Example Scenarios</h2>
|
||||||
<p>
|
<p>
|
||||||
Below are example scenarios with various assertions covering features of our reputation system.
|
Below are example scenarios with various assertions covering features of our reputation system.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
|
||||||
The code for this site is available in <a
|
|
||||||
href="https://gitlab.com/dao-governance-framework/science-publishing-dao/-/tree/main/forum-network/src">GitLab</a>.
|
|
||||||
</p>
|
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="./tests/validation-pool.test.html">Validation Pool</a></li>
|
<li><a href="./tests/validation-pool.test.html">Validation Pool</a></li>
|
||||||
<li><a href="./tests/availability.test.html">Availability + Business</a></li>
|
<li><a href="./tests/availability.test.html">Availability + Business</a></li>
|
||||||
|
@ -66,6 +67,7 @@
|
||||||
<li><a href="./tests/mocha.test.html">Mocha</a></li>
|
<li><a href="./tests/mocha.test.html">Mocha</a></li>
|
||||||
<li><a href="./tests/input.test.html">Input</a></li>
|
<li><a href="./tests/input.test.html">Input</a></li>
|
||||||
<li><a href="./tests/document.test.html">Document</a></li>
|
<li><a href="./tests/document.test.html">Document</a></li>
|
||||||
|
<li><a href="./tests/force-directed.test.html">Force-Directed Graph</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
<h4><a href="./tests/all.test.html">All</a></h4>
|
<h4><a href="./tests/all.test.html">All</a></h4>
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Schema Editor</title>
|
||||||
|
<link type="text/css" rel="stylesheet" href="../index.css" />
|
||||||
|
<script type="module" src="./index.js"></script>
|
||||||
|
<script defer data-domain="dgov.io" src="https://plausible.io/js/script.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2><a href="../">DGF Prototype</a></h2>
|
||||||
|
<h1>Schema Editor</h1>
|
||||||
|
<div id="scene"></div>
|
||||||
|
</body>
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Box } from '../classes/display/box.js';
|
||||||
|
import { Scene } from '../classes/display/scene.js';
|
||||||
|
import { SchemaEditor } from '../classes/supporting/schema.js';
|
||||||
|
|
||||||
|
const rootElement = document.getElementById('scene');
|
||||||
|
const rootBox = new Box('rootBox', rootElement).flex();
|
||||||
|
window.disableSceneControls = true;
|
||||||
|
window.scene = new Scene('Schema Editor', rootBox);
|
||||||
|
window.schemaEditor = new SchemaEditor('new', window.scene);
|
|
@ -40,6 +40,7 @@
|
||||||
<script type="module" src="./scripts/forum/forum11.test.js"></script>
|
<script type="module" src="./scripts/forum/forum11.test.js"></script>
|
||||||
<script type="module" src="./scripts/input.test.js"></script>
|
<script type="module" src="./scripts/input.test.js"></script>
|
||||||
<script type="module" src="./scripts/document.test.js"></script>
|
<script type="module" src="./scripts/document.test.js"></script>
|
||||||
|
<script type="module" src="./scripts/force-directed.test.js"></script>
|
||||||
<script defer class="mocha-init">
|
<script defer class="mocha-init">
|
||||||
mocha.setup({
|
mocha.setup({
|
||||||
ui: 'bdd',
|
ui: 'bdd',
|
||||||
|
|
|
@ -27,6 +27,6 @@
|
||||||
<script defer class="mocha-init">
|
<script defer class="mocha-init">
|
||||||
mocha.setup({
|
mocha.setup({
|
||||||
ui: 'bdd',
|
ui: 'bdd',
|
||||||
});
|
});
|
||||||
window.should = chai.should();
|
window.should = chai.should();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Force-Directed Graph test</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/mocha/mocha.css" />
|
||||||
|
<link type="text/css" rel="stylesheet" href="../index.css" />
|
||||||
|
<script defer data-domain="dgov.io" src="https://plausible.io/js/script.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2><a href="../">DGF Prototype</a></h2>
|
||||||
|
<div id="mocha"></div>
|
||||||
|
<div id="scene"></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/radash/10.7.0/radash.js"
|
||||||
|
integrity="sha512-S207zKWG3iqXqe6msO7/Mr8X3DzzF4u8meFlokHjGtBPTGUhgzVo0lpcqEy0GoiMUdcoct+H+SqzoLsxXbynzg=="
|
||||||
|
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
<script src="https://unpkg.com/chai/chai.js"></script>
|
||||||
|
<script src="https://unpkg.com/mocha/mocha.js"></script>
|
||||||
|
<script defer class="mocha-init">
|
||||||
|
mocha.setup({
|
||||||
|
ui: 'bdd',
|
||||||
|
});
|
||||||
|
chai.should();
|
||||||
|
</script>
|
||||||
|
<script type="module" src="./scripts/force-directed.test.js"></script>
|
|
@ -6,6 +6,7 @@ import { Public } from '../../classes/actors/public.js';
|
||||||
import { PostContent } from '../../classes/supporting/post-content.js';
|
import { PostContent } from '../../classes/supporting/post-content.js';
|
||||||
import { delayOrWait } from '../../classes/display/scene-controls.js';
|
import { delayOrWait } from '../../classes/display/scene-controls.js';
|
||||||
import { mochaRun } from '../../util/helpers.js';
|
import { mochaRun } from '../../util/helpers.js';
|
||||||
|
import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../util/constants.js';
|
||||||
|
|
||||||
const DELAY_INTERVAL = 100;
|
const DELAY_INTERVAL = 100;
|
||||||
const POOL_DURATION = 200;
|
const POOL_DURATION = 200;
|
||||||
|
@ -32,7 +33,7 @@ const setup = async () => {
|
||||||
scene.withTable();
|
scene.withTable();
|
||||||
|
|
||||||
dao = new DAO('DGF', scene);
|
dao = new DAO('DGF', scene);
|
||||||
await dao.setDisplayValue('total rep', () => dao.reputation.getTotal());
|
await dao.setDisplayValue('total rep', () => dao.reputation.getTotal(DEFAULT_REP_TOKEN_TYPE_ID));
|
||||||
|
|
||||||
experts = [];
|
experts = [];
|
||||||
|
|
||||||
|
@ -58,7 +59,7 @@ const setup = async () => {
|
||||||
await pool1.evaluateWinningConditions();
|
await pool1.evaluateWinningConditions();
|
||||||
await delayOrWait(DELAY_INTERVAL);
|
await delayOrWait(DELAY_INTERVAL);
|
||||||
|
|
||||||
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(10);
|
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(10);
|
||||||
|
|
||||||
const { pool: pool2 } = await experts[1].submitPostWithFee(
|
const { pool: pool2 } = await experts[1].submitPostWithFee(
|
||||||
new PostContent({ hello: 'to you as well' })
|
new PostContent({ hello: 'to you as well' })
|
||||||
|
@ -77,8 +78,8 @@ const setup = async () => {
|
||||||
await pool2.evaluateWinningConditions();
|
await pool2.evaluateWinningConditions();
|
||||||
await delayOrWait(DELAY_INTERVAL);
|
await delayOrWait(DELAY_INTERVAL);
|
||||||
|
|
||||||
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(15);
|
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(15);
|
||||||
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(5);
|
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getActiveWorker = async () => {
|
const getActiveWorker = async () => {
|
||||||
|
|
|
@ -0,0 +1,204 @@
|
||||||
|
import { Box } from '../../classes/display/box.js';
|
||||||
|
import { ForceDirectedGraph } from '../../classes/display/force-directed.js';
|
||||||
|
import { Rectangle, Vector } from '../../classes/supporting/geometry/index.js';
|
||||||
|
import { overlapRepulsionForce, targetRadiusForce } from '../../classes/display/pairwise-forces.js';
|
||||||
|
import { delayOrWait } from '../../classes/display/scene-controls.js';
|
||||||
|
import { Scene } from '../../classes/display/scene.js';
|
||||||
|
import { DEFAULT_OVERLAP_BUFFER, EPSILON } from '../../util/constants.js';
|
||||||
|
import { mochaRun } from '../../util/helpers.js';
|
||||||
|
|
||||||
|
const rootElement = document.getElementById('scene');
|
||||||
|
const rootBox = new Box('rootBox', rootElement).flex();
|
||||||
|
window.scene = new Scene('WDG test', rootBox);
|
||||||
|
|
||||||
|
describe('Force-Directed Graph', function tests() {
|
||||||
|
this.timeout(0);
|
||||||
|
|
||||||
|
let graph;
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
graph = (window.graph = new ForceDirectedGraph('test1', window.scene.middleSection.el, {
|
||||||
|
width: 1200, height: 900,
|
||||||
|
}));
|
||||||
|
|
||||||
|
graph.addVertex('v1', 'box1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rectangle should be a polygon with 4 vertices', () => {
|
||||||
|
const rect = new Rectangle([0, 0], [1, 1]);
|
||||||
|
rect.vertices[0].should.eql([0, 0]);
|
||||||
|
rect.vertices[1].should.eql([1, 0]);
|
||||||
|
rect.vertices[2].should.eql([1, 1]);
|
||||||
|
rect.vertices[3].should.eql([0, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should measure extent of overlap', () => {
|
||||||
|
const rects = [
|
||||||
|
new Rectangle([0, 0], [1, 1]),
|
||||||
|
new Rectangle([0.5, 0], [1, 1]),
|
||||||
|
new Rectangle([0, 0.5], [1, 1]),
|
||||||
|
new Rectangle([0.5, 0.5], [1, 1]),
|
||||||
|
new Rectangle([0, 1], [1, 1]),
|
||||||
|
new Rectangle([0, 0], [1, 2]),
|
||||||
|
new Rectangle([0, 0], [0.5, 0.5]),
|
||||||
|
new Rectangle([2, 2], [1, 1]),
|
||||||
|
];
|
||||||
|
rects[0].doesOverlap(rects[1]).should.eql(1);
|
||||||
|
rects[0].doesOverlap(rects[2]).should.eql(1);
|
||||||
|
rects[0].doesOverlap(rects[3]).should.eql(0.5);
|
||||||
|
rects[0].doesOverlap(rects[4]).should.eql(0);
|
||||||
|
rects[0].doesOverlap(rects[5]).should.eql(1);
|
||||||
|
rects[0].doesOverlap(rects[6]).should.eql(1);
|
||||||
|
rects[0].doesOverlap(rects[7]).should.eql(0);
|
||||||
|
|
||||||
|
rects[1].doesOverlap(rects[0]).should.eql(1);
|
||||||
|
rects[2].doesOverlap(rects[0]).should.eql(1);
|
||||||
|
rects[3].doesOverlap(rects[0]).should.eql(0.5);
|
||||||
|
rects[4].doesOverlap(rects[0]).should.eql(0);
|
||||||
|
rects[5].doesOverlap(rects[0]).should.eql(1);
|
||||||
|
rects[6].doesOverlap(rects[0]).should.eql(1);
|
||||||
|
rects[7].doesOverlap(rects[0]).should.eql(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('overlapping boxes should repel', () => {
|
||||||
|
const rect1 = new Rectangle([0, 0], [1, 1]);
|
||||||
|
const rect2 = new Rectangle([0, 0], [1, 2]);
|
||||||
|
rect1.center.should.eql([0.5, 0.5]);
|
||||||
|
rect2.center.should.eql([0.5, 1]);
|
||||||
|
const force1 = overlapRepulsionForce(rect1, rect2, 10);
|
||||||
|
force1.should.eql([0, 10]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adjacent boxes should repel', () => {
|
||||||
|
const rect1 = new Rectangle([0, 0], [1, 1]);
|
||||||
|
const rect2 = new Rectangle([0, 1], [1, 1]);
|
||||||
|
const rect3 = new Rectangle([0, DEFAULT_OVERLAP_BUFFER / 2 + 1], [1, 1]);
|
||||||
|
const rect4 = new Rectangle([0, DEFAULT_OVERLAP_BUFFER + 1], [1, 1]);
|
||||||
|
const rect5 = new Rectangle([DEFAULT_OVERLAP_BUFFER + 1, DEFAULT_OVERLAP_BUFFER + 1], [1, 1]);
|
||||||
|
rect1.doesOverlap(rect2).should.eql(0);
|
||||||
|
rect1.doesOverlap(rect3).should.eql(0);
|
||||||
|
rect1.doesOverlap(rect4).should.eql(0);
|
||||||
|
const force1 = overlapRepulsionForce(rect1, rect2, 10);
|
||||||
|
force1[0].should.eql(0);
|
||||||
|
force1[1].should.be.within(9.99, 10.01);
|
||||||
|
const force2 = overlapRepulsionForce(rect1, rect3, 10);
|
||||||
|
force2[0].should.eql(0);
|
||||||
|
force2[1].should.be.within(4.99, 5.01);
|
||||||
|
const force3 = overlapRepulsionForce(rect1, rect4, 10);
|
||||||
|
force3[0].should.eql(0);
|
||||||
|
force3[1].should.be.within(-0.01, 0.01);
|
||||||
|
const force4 = overlapRepulsionForce(rect1, rect5, 10);
|
||||||
|
force4[0].should.be.within(-0.01, 0.01);
|
||||||
|
force4[1].should.be.within(-0.01, 0.01);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('boxes at target radius should have no net force', () => {
|
||||||
|
const rect1 = new Rectangle([0, 0], [1, 1]);
|
||||||
|
const rect2 = new Rectangle([11, 0], [1, 1]);
|
||||||
|
rect1.center.should.eql([0.5, 0.5]);
|
||||||
|
rect2.center.should.eql([11.5, 0.5]);
|
||||||
|
const force = targetRadiusForce(rect1, rect2, 10);
|
||||||
|
force[0].should.be.within(-0.01, 0.01);
|
||||||
|
force[1].should.be.within(-0.01, 0.01);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can construct a unit vector', () => {
|
||||||
|
Vector.unitVector(0, 2).should.eql([1, 0]);
|
||||||
|
Vector.unitVector(1, 2).should.eql([0, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalized vector should have length = 1', () => {
|
||||||
|
const v = Vector.from([2, 0]);
|
||||||
|
const u = v.normalize();
|
||||||
|
u.magnitude.should.be.within(1 - EPSILON, 1 + EPSILON);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('random unit vector should have length = 1', () => {
|
||||||
|
const u = Vector.randomUnitVector(2);
|
||||||
|
u.magnitude.should.be.within(1 - EPSILON, 1 + EPSILON);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can compute intersection of line with rectangle', () => {
|
||||||
|
const rect = new Rectangle([100, 100], [100, 100]);
|
||||||
|
{
|
||||||
|
const intersect = rect.lineIntersect([0, 150], [1, 0]);
|
||||||
|
intersect[0].should.be.within(99.99, 100.01);
|
||||||
|
intersect[1].should.eql(150);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const intersect = rect.lineIntersect([150, 0], [0, 1]);
|
||||||
|
intersect[0].should.eql(150);
|
||||||
|
intersect[1].should.be.within(99.99, 100.01);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const intersect = rect.lineIntersect([0, 0], [1, 1]);
|
||||||
|
intersect[0].should.be.within(99.99, 100.01);
|
||||||
|
intersect[1].should.be.within(99.99, 100.01);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const intersect = rect.lineIntersect([0, 150], [-1, 0]);
|
||||||
|
(intersect === null).should.be.true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can add a second box to the graph', async () => {
|
||||||
|
await delayOrWait(500);
|
||||||
|
const v = graph.addVertex('v1', 'box2');
|
||||||
|
v.setProperty('prop', 'value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can add an edge to the graph', async () => {
|
||||||
|
await delayOrWait(500);
|
||||||
|
graph.addEdge('e1', 'box1', 'box2', 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('runs until reaching equilibrium', async () => {
|
||||||
|
await graph.runUntilEquilibrium();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can add 10 random nodes', async () => {
|
||||||
|
for (let i = 3; i <= 10; i++) {
|
||||||
|
await delayOrWait(200);
|
||||||
|
const v = graph.addVertex('v2', `box${i}`);
|
||||||
|
v.setProperty('prop', 'value');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can add 10 random edges', async () => {
|
||||||
|
await delayOrWait(500);
|
||||||
|
for (let i = 1; i <= 10; i++) {
|
||||||
|
let from;
|
||||||
|
let to;
|
||||||
|
do {
|
||||||
|
from = graph.nodes[Math.floor(Math.random() * graph.nodes.length)];
|
||||||
|
to = graph.nodes[Math.floor(Math.random() * graph.nodes.length)];
|
||||||
|
} while (from.name === to.name && !graph.getEdge('one', from.name, to.name));
|
||||||
|
await delayOrWait(200);
|
||||||
|
graph.addEdge('one', from.name, to.name, Math.floor(Math.random() * 100) / 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skip('can add 10 more random nodes', async () => {
|
||||||
|
for (let i = 11; i <= 20; i++) {
|
||||||
|
await delayOrWait(200);
|
||||||
|
const v = graph.addVertex('v3', `box${i}`);
|
||||||
|
v.setProperty('prop', Math.random() * 10000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can add 10 more random edges', async () => {
|
||||||
|
await delayOrWait(500);
|
||||||
|
for (let i = 11; i <= 20; i++) {
|
||||||
|
let from;
|
||||||
|
let to;
|
||||||
|
do {
|
||||||
|
from = graph.nodes[Math.floor(Math.random() * graph.nodes.length)];
|
||||||
|
to = graph.nodes[Math.floor(Math.random() * graph.nodes.length)];
|
||||||
|
} while (from.name === to.name && !graph.getEdge('two', from.name, to.name));
|
||||||
|
await delayOrWait(200);
|
||||||
|
graph.addEdge('two', from.name, to.name, Math.floor(Math.random() * 100) / 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
mochaRun();
|
|
@ -4,6 +4,7 @@ import { Expert } from '../../classes/actors/expert.js';
|
||||||
import { PostContent } from '../../classes/supporting/post-content.js';
|
import { PostContent } from '../../classes/supporting/post-content.js';
|
||||||
import { DAO } from '../../classes/dao/dao.js';
|
import { DAO } from '../../classes/dao/dao.js';
|
||||||
import { delayOrWait } from '../../classes/display/scene-controls.js';
|
import { delayOrWait } from '../../classes/display/scene-controls.js';
|
||||||
|
import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../util/constants.js';
|
||||||
|
|
||||||
export class ForumTest {
|
export class ForumTest {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
|
@ -86,6 +87,6 @@ export class ForumTest {
|
||||||
|
|
||||||
await this.newExpert();
|
await this.newExpert();
|
||||||
|
|
||||||
await this.dao.addComputedValue('total value', () => this.dao.reputation.getTotal());
|
await this.dao.addComputedValue('total rep', () => this.dao.reputation.getTotal(DEFAULT_REP_TOKEN_TYPE_ID));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../../util/constants.js';
|
||||||
import { mochaRun } from '../../../util/helpers.js';
|
import { mochaRun } from '../../../util/helpers.js';
|
||||||
import { ForumTest } from '../forum.test-util.js';
|
import { ForumTest } from '../forum.test-util.js';
|
||||||
|
|
||||||
|
@ -34,9 +35,9 @@ describe('Forum', function tests() {
|
||||||
await forumTest.addPost(authors, 10);
|
await forumTest.addPost(authors, 10);
|
||||||
forum.getPost(posts[0]).value.should.equal(10);
|
forum.getPost(posts[0]).value.should.equal(10);
|
||||||
|
|
||||||
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(5);
|
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
|
||||||
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(2.5);
|
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(2.5);
|
||||||
dao.reputation.valueOwnedBy(experts[2].reputationPublicKey).should.equal(2.5);
|
dao.reputation.valueOwnedBy(experts[2].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(2.5);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../../util/constants.js';
|
||||||
import { mochaRun } from '../../../util/helpers.js';
|
import { mochaRun } from '../../../util/helpers.js';
|
||||||
import { ForumTest } from '../forum.test-util.js';
|
import { ForumTest } from '../forum.test-util.js';
|
||||||
|
|
||||||
|
@ -33,9 +34,9 @@ describe('Forum', function tests() {
|
||||||
await forumTest.addPost(authors, 10);
|
await forumTest.addPost(authors, 10);
|
||||||
forum.getPost(posts[0]).value.should.equal(10);
|
forum.getPost(posts[0]).value.should.equal(10);
|
||||||
|
|
||||||
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(5);
|
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
|
||||||
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(5);
|
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
|
||||||
dao.reputation.valueOwnedBy(experts[2].reputationPublicKey).should.equal(0);
|
dao.reputation.valueOwnedBy(experts[2].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Post2 with two authors, one shared with Post1', async () => {
|
it('Post2 with two authors, one shared with Post1', async () => {
|
||||||
|
@ -46,9 +47,9 @@ describe('Forum', function tests() {
|
||||||
await forumTest.addPost(authors, 10);
|
await forumTest.addPost(authors, 10);
|
||||||
forum.getPost(posts[0]).value.should.equal(10);
|
forum.getPost(posts[0]).value.should.equal(10);
|
||||||
|
|
||||||
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(5);
|
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
|
||||||
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(10);
|
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(10);
|
||||||
dao.reputation.valueOwnedBy(experts[2].reputationPublicKey).should.equal(5);
|
dao.reputation.valueOwnedBy(experts[2].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Box } from '../../classes/display/box.js';
|
import { Box } from '../../classes/display/box.js';
|
||||||
import { Scene } from '../../classes/display/scene.js';
|
import { Scene } from '../../classes/display/scene.js';
|
||||||
|
import { WDGDiagram } from '../../classes/display/wdg-mermaid-ui.js';
|
||||||
import { WeightedDirectedGraph } from '../../classes/supporting/wdg.js';
|
import { WeightedDirectedGraph } from '../../classes/supporting/wdg.js';
|
||||||
import { mochaRun } from '../../util/helpers.js';
|
import { mochaRun } from '../../util/helpers.js';
|
||||||
|
|
||||||
|
@ -13,7 +14,7 @@ describe('Weighted Directed Graph', function tests() {
|
||||||
let graph;
|
let graph;
|
||||||
|
|
||||||
before(() => {
|
before(() => {
|
||||||
graph = (window.graph = new WeightedDirectedGraph('test1', window.scene)).withFlowchart();
|
graph = (window.graph = new WDGDiagram('test1', window.scene)).withFlowchart();
|
||||||
|
|
||||||
graph.addVertex('v1', {});
|
graph.addVertex('v1', {});
|
||||||
graph.addVertex('v1', {});
|
graph.addVertex('v1', {});
|
||||||
|
@ -150,20 +151,4 @@ describe('Weighted Directed Graph', function tests() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Editable', () => {
|
|
||||||
let graph;
|
|
||||||
|
|
||||||
it('should be editable', () => {
|
|
||||||
graph = (window.graph2 = new WeightedDirectedGraph('test2', window.scene, { editable: true })).withFlowchart();
|
|
||||||
|
|
||||||
graph.addVertex('v1', {});
|
|
||||||
graph.addVertex('v2', {});
|
|
||||||
graph.addVertex('v3', {});
|
|
||||||
|
|
||||||
graph.addEdge('e1', 2, 1, 1);
|
|
||||||
graph.addEdge('e2', 1, 0, 0.5);
|
|
||||||
graph.addEdge('e3', 2, 0, 0.25);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
mochaRun();
|
mochaRun();
|
||||||
|
|
|
@ -1,14 +1,28 @@
|
||||||
export const EPSILON = 2.23e-16;
|
|
||||||
|
|
||||||
export const INCINERATOR_ADDRESS = '0';
|
|
||||||
|
|
||||||
export const EdgeTypes = {
|
export const EdgeTypes = {
|
||||||
CITATION: 'citation',
|
CITATION: 'citation',
|
||||||
BALANCE: 'balance',
|
BALANCE: 'balance',
|
||||||
AUTHOR: 'author',
|
AUTHOR: 'author',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VertexTypes = {
|
export const VertexTypes = {
|
||||||
POST: 'post',
|
POST: 'post',
|
||||||
AUTHOR: 'author',
|
AUTHOR: 'author',
|
||||||
};
|
};
|
||||||
|
export const ARROWHEAD_LENGTH = 12;
|
||||||
|
export const ARROWHEAD_WIDTH = 6;
|
||||||
|
export const CENTRAL_RESTORING_FORCE = 100;
|
||||||
|
export const DEFAULT_OVERLAP_BUFFER = 100;
|
||||||
|
export const DEFAULT_OVERLAP_FORCE = 400;
|
||||||
|
export const DEFAULT_REP_TOKEN_TYPE_ID = 0;
|
||||||
|
export const DEFAULT_TARGET_RADIUS = 200;
|
||||||
|
export const DEFAULT_TIME_STEP = 0.1;
|
||||||
|
export const DEFAULT_DISTANCE_FACTOR = 0.5;
|
||||||
|
export const EPSILON = 2.23e-16;
|
||||||
|
export const INCINERATOR_ADDRESS = '0';
|
||||||
|
export const MAXIMUM_STEPS = 500;
|
||||||
|
export const MINIMUM_FORCE = 1;
|
||||||
|
export const MINIMUM_VELOCITY = 1;
|
||||||
|
export const MINIMUM_STEPS = 10;
|
||||||
|
export const OVERLAP_THRESHOLD_RANDOMIZE = 0.5;
|
||||||
|
export const TIME_DILATION_FACTOR = 500;
|
||||||
|
export const TRANSLATION_VELOCITY_FACTOR = 0.2;
|
||||||
|
export const VISCOSITY_FACTOR = 0.7;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Box } from '../classes/display/box.js';
|
import { Box } from '../classes/display/box.js';
|
||||||
import { Scene } from '../classes/display/scene.js';
|
import { Scene } from '../classes/display/scene.js';
|
||||||
import { WeightedDirectedGraph } from '../classes/supporting/wdg.js';
|
import { WDGDiagram } from '../classes/display/wdg-mermaid-ui.js';
|
||||||
|
|
||||||
const rootElement = document.getElementById('scene');
|
const rootElement = document.getElementById('scene');
|
||||||
const rootBox = new Box('rootBox', rootElement).flex();
|
const rootBox = new Box('rootBox', rootElement).flex();
|
||||||
window.disableSceneControls = true;
|
window.disableSceneControls = true;
|
||||||
window.scene = new Scene('WDG Editor', rootBox);
|
window.scene = new Scene('WDG Editor', rootBox);
|
||||||
window.graph = new WeightedDirectedGraph('new', window.scene, { editable: true }).withFlowchart();
|
window.graph = new WDGDiagram('new', window.scene, { editable: true }).withFlowchart();
|
||||||
|
|
Loading…
Reference in New Issue