Compare commits
1 Commits
Author | SHA1 | Date |
---|---|---|
Ladd Hoffman | 8a23ee63d6 |
|
@ -1,17 +0,0 @@
|
|||
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;
|
||||
}
|
||||
const authorEdges = authorVertex.getEdges(EdgeTypes.AUTHOR, false);
|
||||
const tokenValues = authorEdges.map(({ data: { tokenAddress } }) => this.dao.reputation.valueOf(tokenAddress));
|
||||
const tokenValues = authorEdges.map(({ data: { tokenId } }) => this.dao.reputation.valueOf(tokenId));
|
||||
return tokenValues.reduce((value, total) => total += value, 0);
|
||||
}
|
||||
|
||||
|
@ -42,7 +42,7 @@ export class Expert extends ReputationHolder {
|
|||
await this.actions.submitPost.log(this, post);
|
||||
const postId = post.id;
|
||||
const pool = await this.initiateValidationPool({ fee, postId }, params);
|
||||
this.tokens.push(pool.tokenAddress);
|
||||
this.tokens.push(pool.tokenId);
|
||||
return { postId, pool };
|
||||
}
|
||||
|
||||
|
@ -53,7 +53,7 @@ export class Expert extends ReputationHolder {
|
|||
postId,
|
||||
fee,
|
||||
}, params);
|
||||
this.tokens.push(pool.tokenAddress);
|
||||
this.tokens.push(pool.tokenId);
|
||||
return pool;
|
||||
}
|
||||
|
||||
|
@ -68,7 +68,7 @@ export class Expert extends ReputationHolder {
|
|||
`(${position ? 'for' : 'against'}, stake: ${amount})`,
|
||||
);
|
||||
return validationPool.stake(this.reputationPublicKey, {
|
||||
position, amount, lockingTime, tokenAddress: this.tokens[0],
|
||||
position, amount, lockingTime, tokenId: this.tokens[0],
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -80,7 +80,7 @@ export class Expert extends ReputationHolder {
|
|||
);
|
||||
this.workerId = await this.dao.availability.register(this.reputationPublicKey, {
|
||||
stakeAmount,
|
||||
tokenAddress: this.tokens[0],
|
||||
tokenId: this.tokens[0],
|
||||
duration,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3,9 +3,9 @@ import { Actor } from '../display/actor.js';
|
|||
import { CryptoUtil } from '../supporting/crypto.js';
|
||||
|
||||
class Worker {
|
||||
constructor(reputationPublicKey, tokenAddress, stakeAmount, duration) {
|
||||
constructor(reputationPublicKey, tokenId, stakeAmount, duration) {
|
||||
this.reputationPublicKey = reputationPublicKey;
|
||||
this.tokenAddress = tokenAddress;
|
||||
this.tokenId = tokenId;
|
||||
this.stakeAmount = stakeAmount;
|
||||
this.duration = duration;
|
||||
this.available = true;
|
||||
|
@ -28,11 +28,11 @@ export class Availability extends Actor {
|
|||
this.workers = new Map();
|
||||
}
|
||||
|
||||
register(reputationPublicKey, { stakeAmount, tokenAddress, duration }) {
|
||||
register(reputationPublicKey, { stakeAmount, tokenId, duration }) {
|
||||
// TODO: Should be signed by token owner
|
||||
this.dao.reputation.lock(tokenAddress, stakeAmount, duration);
|
||||
this.dao.reputation.lock(tokenId, stakeAmount, duration);
|
||||
const workerId = CryptoUtil.randomUUID();
|
||||
this.workers.set(workerId, new Worker(reputationPublicKey, tokenAddress, stakeAmount, duration));
|
||||
this.workers.set(workerId, new Worker(reputationPublicKey, tokenId, stakeAmount, duration));
|
||||
return workerId;
|
||||
}
|
||||
|
||||
|
|
|
@ -87,7 +87,7 @@ export class Business extends Actor {
|
|||
});
|
||||
|
||||
await pool.stake(reputationPublicKey, {
|
||||
tokenAddress: request.worker.tokenAddress,
|
||||
tokenId: request.worker.tokenId,
|
||||
amount: request.worker.stakeAmount,
|
||||
position: true,
|
||||
});
|
||||
|
|
|
@ -5,7 +5,6 @@ import { Availability } from './availability.js';
|
|||
import { Business } from './business.js';
|
||||
import { Voter } from '../supporting/voter.js';
|
||||
import { Actor } from '../display/actor.js';
|
||||
import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../util/constants.js';
|
||||
|
||||
/**
|
||||
* Purpose:
|
||||
|
@ -50,13 +49,15 @@ export class DAO extends Actor {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} param0.reputationTypeId
|
||||
* @returns {number}
|
||||
*/
|
||||
getActiveAvailableReputation({ reputationTypeId = DEFAULT_REP_TOKEN_TYPE_ID }) {
|
||||
getActiveReputation() {
|
||||
return this.listActiveVoters()
|
||||
.map(({ reputationPublicKey }) => this.reputation.availableValueOwnedBy(reputationPublicKey, reputationTypeId))
|
||||
.map(({ reputationPublicKey }) => this.reputation.valueOwnedBy(reputationPublicKey))
|
||||
.reduce((acc, cur) => (acc += cur), 0);
|
||||
}
|
||||
|
||||
getActiveAvailableReputation() {
|
||||
return this.listActiveVoters()
|
||||
.map(({ reputationPublicKey }) => this.reputation.availableValueOwnedBy(reputationPublicKey))
|
||||
.reduce((acc, cur) => (acc += cur), 0);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { WeightedDirectedGraph } from '../supporting/wdg.js';
|
||||
import { Action } from '../display/action.js';
|
||||
import { Actor } from '../display/actor.js';
|
||||
import { ReputationHolder } from '../reputation/reputation-holder.js';
|
||||
|
@ -5,7 +6,6 @@ import { displayNumber } from '../../util/helpers.js';
|
|||
import {
|
||||
EPSILON, INCINERATOR_ADDRESS, EdgeTypes, VertexTypes,
|
||||
} from '../../util/constants.js';
|
||||
import { WDGDiagram } from '../display/wdg-mermaid-ui.js';
|
||||
|
||||
class Post extends Actor {
|
||||
constructor(forum, senderId, postContent) {
|
||||
|
@ -15,8 +15,8 @@ class Post extends Actor {
|
|||
this.forum = forum;
|
||||
this.id = postContent.id ?? name;
|
||||
this.senderId = senderId;
|
||||
this.values = new Map();
|
||||
this.initialValues = new Map();
|
||||
this.value = 0;
|
||||
this.initialValue = 0;
|
||||
this.authors = postContent.authors;
|
||||
this.citations = postContent.citations;
|
||||
this.title = postContent.title;
|
||||
|
@ -44,7 +44,7 @@ export class Forum extends ReputationHolder {
|
|||
super(name, scene);
|
||||
this.dao = dao;
|
||||
this.id = this.reputationPublicKey;
|
||||
this.graph = new WDGDiagram('forum', scene);
|
||||
this.graph = new WeightedDirectedGraph('forum', scene);
|
||||
this.actions = {
|
||||
propagate: new Action('propagate', scene),
|
||||
confirm: new Action('confirm', scene),
|
||||
|
@ -83,21 +83,16 @@ export class Forum extends ReputationHolder {
|
|||
// getContract(type) { }
|
||||
|
||||
async onValidate({
|
||||
pool, postId, tokenAddress, referenceChainLimit, leachingValue,
|
||||
pool, postId, tokenId, referenceChainLimit, leachingValue,
|
||||
}) {
|
||||
console.log('onValidate', { pool, postId, tokenAddress });
|
||||
|
||||
// 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)]));
|
||||
console.log('onValidate', { pool, postId, tokenId });
|
||||
const initialValue = this.dao.reputation.valueOf(tokenId);
|
||||
const postVertex = this.graph.getVertex(postId);
|
||||
const post = postVertex.data;
|
||||
post.setStatus('Validated');
|
||||
post.initialValues = initialValues;
|
||||
post.initialValue = initialValue;
|
||||
|
||||
const addAuthorToGraph = (publicKey, weight, authorTokenAddress) => {
|
||||
const addAuthorToGraph = (publicKey, weight, authorTokenId) => {
|
||||
// For graph display purposes, we want to use the existing Expert actors from the current scene.
|
||||
const author = this.scene.findActor(({ reputationPublicKey }) => reputationPublicKey === publicKey);
|
||||
author.setDisplayValue('reputation', () => author.getReputation());
|
||||
|
@ -110,7 +105,7 @@ export class Forum extends ReputationHolder {
|
|||
postVertex,
|
||||
authorVertex,
|
||||
weight,
|
||||
{ tokenAddress: authorTokenAddress },
|
||||
{ tokenId: authorTokenId },
|
||||
{ hide: author.options.hide },
|
||||
);
|
||||
};
|
||||
|
@ -119,18 +114,16 @@ export class Forum extends ReputationHolder {
|
|||
// If no authors are specified, treat the sender as the sole author.
|
||||
// TODO: Verify that cumulative author weight == 1.
|
||||
if (!post.authors?.length) {
|
||||
addAuthorToGraph(post.senderId, 1, tokenAddress);
|
||||
addAuthorToGraph(post.senderId, 1, tokenId);
|
||||
} else {
|
||||
for (const { publicKey, weight } of post.authors) {
|
||||
// If the sender is also listed among the authors, do not mint them an additional token.
|
||||
const authorTokenAddress = (publicKey === post.senderId)
|
||||
? tokenAddress
|
||||
: this.dao.reputation.mintBatch(this.id, pool.reputationTypeIds, pool.reputationTypeIds.map(() => 0));
|
||||
addAuthorToGraph(publicKey, weight, authorTokenAddress);
|
||||
const authorTokenId = (publicKey === post.senderId) ? tokenId : this.dao.reputation.mint(this.id, 0);
|
||||
addAuthorToGraph(publicKey, weight, authorTokenId);
|
||||
}
|
||||
// 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)) {
|
||||
addAuthorToGraph(post.senderId, 0, tokenAddress);
|
||||
addAuthorToGraph(post.senderId, 0, tokenId);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -141,7 +134,7 @@ export class Forum extends ReputationHolder {
|
|||
{ to: postVertex, from: { data: pool } },
|
||||
{
|
||||
rewardsAccumulator,
|
||||
increments: initialValues,
|
||||
increment: initialValue,
|
||||
referenceChainLimit,
|
||||
leachingValue,
|
||||
},
|
||||
|
@ -149,16 +142,16 @@ export class Forum extends ReputationHolder {
|
|||
|
||||
// Apply computed rewards to update values of tokens
|
||||
for (const [authorEdge, amount] of rewardsAccumulator) {
|
||||
const { to: authorVertex, data: { tokenAddress: authorTokenAddress } } = authorEdge;
|
||||
const { to: authorVertex, data: { tokenId: authorTokenId } } = authorEdge;
|
||||
const { data: author } = authorVertex;
|
||||
// The primary author gets the validation pool minted token.
|
||||
// So we don't need to transfer any reputation to the primary author.
|
||||
// Their reward will be the remaining balance after all other transfers.
|
||||
if (authorTokenAddress !== tokenAddress) {
|
||||
if (authorTokenId !== tokenId) {
|
||||
if (amount < 0) {
|
||||
this.dao.reputation.transferValueFrom(authorTokenAddress, tokenAddress, -amount);
|
||||
this.dao.reputation.transferValueFrom(authorTokenId, tokenId, -amount);
|
||||
} else {
|
||||
this.dao.reputation.transferValueFrom(tokenAddress, authorTokenAddress, amount);
|
||||
this.dao.reputation.transferValueFrom(tokenId, authorTokenId, amount);
|
||||
}
|
||||
await author.computeDisplayValues((label, value) => authorVertex.setProperty(label, value));
|
||||
authorVertex.displayVertex();
|
||||
|
@ -174,8 +167,8 @@ export class Forum extends ReputationHolder {
|
|||
for (const authorEdge of postVertex.getEdges(EdgeTypes.AUTHOR, true)) {
|
||||
const authorVertex = authorEdge.to;
|
||||
const author = authorVertex.data;
|
||||
const { tokenAddress: authorTokenAddress } = authorEdge.data;
|
||||
this.dao.reputation.transfer(this.id, author.reputationPublicKey, authorTokenAddress);
|
||||
const { tokenId: authorTokenId } = authorEdge.data;
|
||||
this.dao.reputation.transfer(this.id, author.reputationPublicKey, authorTokenId);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -185,7 +178,7 @@ export class Forum extends ReputationHolder {
|
|||
*/
|
||||
async propagateValue(edge, {
|
||||
rewardsAccumulator,
|
||||
increments,
|
||||
increment,
|
||||
depth = 0,
|
||||
initialNegative = false,
|
||||
referenceChainLimit,
|
||||
|
@ -193,9 +186,7 @@ export class Forum extends ReputationHolder {
|
|||
}) {
|
||||
const postVertex = edge.to;
|
||||
const post = postVertex.data;
|
||||
const incrementsStrArray = Array.from(increments.entries()).map(([k, v]) => `${k} ${v}`);
|
||||
const incrementsStr = `(${incrementsStrArray.join(')(')})`;
|
||||
this.actions.propagate.log(edge.from.data, post, incrementsStr);
|
||||
this.actions.propagate.log(edge.from.data, post, `(${increment})`);
|
||||
|
||||
if (!!referenceChainLimit && depth > referenceChainLimit) {
|
||||
this.actions.propagate.log(
|
||||
|
@ -205,28 +196,25 @@ export class Forum extends ReputationHolder {
|
|||
null,
|
||||
'-x',
|
||||
);
|
||||
return increments;
|
||||
return increment;
|
||||
}
|
||||
|
||||
console.log('propagateValue start', {
|
||||
from: edge.from.id ?? edge.from,
|
||||
to: edge.to.id,
|
||||
depth,
|
||||
values: post.values,
|
||||
increments,
|
||||
value: post.value,
|
||||
increment,
|
||||
initialNegative,
|
||||
});
|
||||
|
||||
const propagate = async (positive) => {
|
||||
const totalOutboundAmounts = increments.map(() => 0);
|
||||
let totalOutboundAmount = 0;
|
||||
const citationEdges = postVertex.getEdges(EdgeTypes.CITATION, true)
|
||||
.filter(({ weight }) => (positive ? weight > 0 : weight < 0));
|
||||
for (const citationEdge of citationEdges) {
|
||||
const { weight } = citationEdge;
|
||||
const outboundAmounts = increments.map((increment) => weight * increment);
|
||||
const refundsFromOutbound = increments.map(() => 0);
|
||||
for (let idx = 0; idx < outboundAmounts.length; idx++) {
|
||||
let outboundAmount = outboundAmounts[idx];
|
||||
let outboundAmount = weight * increment;
|
||||
if (Math.abs(outboundAmount) > EPSILON) {
|
||||
const balanceToOutbound = this.graph.getEdgeWeight(EdgeTypes.BALANCE, citationEdge.from, citationEdge.to)
|
||||
?? 0;
|
||||
|
@ -240,7 +228,7 @@ export class Forum extends ReputationHolder {
|
|||
this.actions.propagate.log(
|
||||
citationEdge.from.data,
|
||||
{ name: 'Incinerator' },
|
||||
incrementsStr,
|
||||
`(${increment})`,
|
||||
undefined,
|
||||
'-x',
|
||||
);
|
||||
|
@ -248,7 +236,7 @@ export class Forum extends ReputationHolder {
|
|||
}
|
||||
// Reputation sent to the incinerator is burned! This means it is deducted from the sender,
|
||||
// without increasing the value of any other token.
|
||||
this.actions.propagate.log(citationEdge.from.data, { name: 'Incinerator' }, incrementsStr);
|
||||
this.actions.propagate.log(citationEdge.from.data, { name: 'Incinerator' }, `(${increment})`);
|
||||
} else {
|
||||
// We need to ensure that we at most undo the prior effects of this post
|
||||
if (initialNegative) {
|
||||
|
@ -281,38 +269,32 @@ export class Forum extends ReputationHolder {
|
|||
citationEdge.to,
|
||||
balanceToOutbound + outboundAmount,
|
||||
);
|
||||
refundsFromOutbound[idx] = refundFromOutbound;
|
||||
totalOutboundAmounts[idx] += outboundAmount;
|
||||
}
|
||||
const refundStr = refundsFromOutbound.map((refund) => displayNumber(refund)).join('/');
|
||||
totalOutboundAmount += outboundAmount;
|
||||
|
||||
this.actions.confirm.log(
|
||||
citationEdge.to.data,
|
||||
citationEdge.from.data,
|
||||
`(refund: ${refundStr}, leach: ${outboundAmount * leachingValue})`,
|
||||
`(refund: ${displayNumber(refundFromOutbound)}, leach: ${outboundAmount * leachingValue})`,
|
||||
undefined,
|
||||
'-->>',
|
||||
);
|
||||
}
|
||||
}
|
||||
return totalOutboundAmounts;
|
||||
return totalOutboundAmount;
|
||||
};
|
||||
|
||||
// First, leach value via negative citations
|
||||
const totalLeachingAmounts = await propagate(false);
|
||||
for (let idx = 0; idx < totalLeachingAmounts.length; idx++) {
|
||||
increments[idx] -= totalLeachingAmounts[idx] * leachingValue;
|
||||
}
|
||||
const totalLeachingAmount = await propagate(false);
|
||||
increment -= totalLeachingAmount * leachingValue;
|
||||
|
||||
// Now propagate value via positive citations
|
||||
const totalDonationAmounts = await propagate(true);
|
||||
for (let idx = 0; idx < totalDonationAmounts.length; idx++) {
|
||||
increments[idx] -= totalDonationAmounts[idx] * leachingValue;
|
||||
}
|
||||
const totalDonationAmount = await propagate(true);
|
||||
increment -= totalDonationAmount * leachingValue;
|
||||
|
||||
// Apply the remaining increment to the present post
|
||||
const rawNewValues = post.values.map((value, idx) => value + increments[idx]);
|
||||
const newValues = rawNewValues.map((rawNewValue) => Math.max(0, rawNewValue));
|
||||
const appliedIncrements = newValue - post.value;
|
||||
const rawNewValue = post.value + increment;
|
||||
const newValue = Math.max(0, rawNewValue);
|
||||
const appliedIncrement = newValue - post.value;
|
||||
const refundToInbound = increment - appliedIncrement;
|
||||
|
||||
// Apply reputation effects to post authors, not to the post directly
|
||||
|
|
|
@ -2,7 +2,6 @@ import { ReputationHolder } from '../reputation/reputation-holder.js';
|
|||
import { Stake } from '../supporting/stake.js';
|
||||
import { Action } from '../display/action.js';
|
||||
import { displayNumber } from '../../util/helpers.js';
|
||||
import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../util/constants.js';
|
||||
|
||||
const params = {
|
||||
/* Validation Pool parameters */
|
||||
|
@ -52,7 +51,6 @@ export class ValidationPool extends ReputationHolder {
|
|||
duration,
|
||||
tokenLossRatio,
|
||||
contentiousDebate = false,
|
||||
reputationTypes,
|
||||
},
|
||||
name,
|
||||
scene,
|
||||
|
@ -71,16 +69,6 @@ export class ValidationPool extends ReputationHolder {
|
|||
this.actions.initiate.log(fromActor, this, `(fee: ${fee})`);
|
||||
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
|
||||
|
@ -142,34 +130,22 @@ export class ValidationPool extends ReputationHolder {
|
|||
this.duration = duration;
|
||||
this.tokenLossRatio = tokenLossRatio;
|
||||
this.contentiousDebate = contentiousDebate;
|
||||
|
||||
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.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: mintTotal * params.stakeForAuthor * weight,
|
||||
tokenAddress: this.tokenAddress,
|
||||
reputationTypeId,
|
||||
amount: this.mintedValue * params.stakeForAuthor,
|
||||
tokenId: this.tokenId,
|
||||
});
|
||||
this.stake(this.id, {
|
||||
position: false,
|
||||
amount: this.mintedValue * (1 - params.stakeForAuthor) * weight,
|
||||
tokenAddress: this.tokenAddress,
|
||||
reputationTypeId,
|
||||
amount: this.mintedValue * (1 - params.stakeForAuthor),
|
||||
tokenId: this.tokenId,
|
||||
});
|
||||
}
|
||||
|
||||
this.actions.mint.log(this, this, `(${mintTotal})`);
|
||||
this.actions.mint.log(this, this, `(${this.mintedValue})`);
|
||||
|
||||
// Keep a record of voters and their votes
|
||||
this.dao.addVoteRecord(reputationPublicKey, this);
|
||||
|
@ -198,38 +174,39 @@ export class ValidationPool extends ReputationHolder {
|
|||
}
|
||||
|
||||
/**
|
||||
* @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} outcome: null --> all entries. Otherwise filters to position === outcome.
|
||||
* @param {boolean} options.excludeSystem: Whether to exclude votes cast during pool initialization
|
||||
* @returns stake[]
|
||||
*/
|
||||
getStakes({ outcome, tokenTypeId, excludeSystem }) {
|
||||
getStakes(outcome, { excludeSystem }) {
|
||||
return Array.from(this.stakes.values())
|
||||
.filter((stake) => tokenTypeId === null || stake.tokenTypeId === tokenTypeId)
|
||||
.filter(({ tokenAddress }) => !excludeSystem || tokenAddress !== this.tokenAddress)
|
||||
.filter(({ tokenId }) => !excludeSystem || tokenId !== this.tokenId)
|
||||
.filter(({ position }) => outcome === null || position === outcome);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} options.outcome: null --> all entries. Otherwise filters to position === outcome.
|
||||
* @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
|
||||
* @returns number
|
||||
*/
|
||||
getStakedAmount({ outcome, tokenTypeId }) {
|
||||
return this.getStakes({ outcome, tokenTypeId, excludeSystem: false })
|
||||
.map((stake) => stake.getAmount({ lockingTimeExponent: params.lockingTimeExponent }))
|
||||
.reduce((total, amount) => (total += amount), 0);
|
||||
getTotalStakedOnPost(outcome) {
|
||||
return this.getStakes(outcome, { excludeSystem: false })
|
||||
.map((stake) => stake.getStakeValue({ lockingTimeExponent: params.lockingTimeExponent }))
|
||||
.reduce((acc, cur) => (acc += cur), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stake reputation in favor of a given outcome for this validation pool.
|
||||
*
|
||||
* @param {*} reputationPublicKey
|
||||
* @param {object} opts
|
||||
* @param {boolean} outcome: null --> all entries. Otherwise filters to position === outcome.
|
||||
* @returns number
|
||||
*/
|
||||
async stake(reputationPublicKey, {
|
||||
tokenAddress, tokenTypeId = DEFAULT_REP_TOKEN_TYPE_ID, position, amount, lockingTime = 0,
|
||||
}) {
|
||||
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, {
|
||||
tokenId, position, amount, lockingTime = 0,
|
||||
}) {
|
||||
if (this.state === ValidationPoolStates.CLOSED) {
|
||||
throw new Error(`Validation pool ${this.id} is closed.`);
|
||||
}
|
||||
|
@ -240,17 +217,17 @@ export class ValidationPool extends ReputationHolder {
|
|||
);
|
||||
}
|
||||
|
||||
if (reputationPublicKey !== this.dao.reputation.ownerOf(tokenAddress)) {
|
||||
if (reputationPublicKey !== this.dao.reputation.ownerOf(tokenId)) {
|
||||
throw new Error('Reputation may only be staked by its owner!');
|
||||
}
|
||||
|
||||
const stake = new Stake({
|
||||
tokenAddress, tokenTypeId, position, amount, lockingTime,
|
||||
tokenId, position, amount, lockingTime,
|
||||
});
|
||||
this.stakes.add(stake);
|
||||
|
||||
// Transfer staked amount from the sender to the validation pool
|
||||
this.dao.reputation.transferValueFrom(tokenAddress, this.tokenAddress, tokenTypeId, amount);
|
||||
this.dao.reputation.transferValueFrom(tokenId, this.tokenId, amount);
|
||||
|
||||
// Keep a record of voters and their votes
|
||||
if (reputationPublicKey !== this.id) {
|
||||
|
@ -266,10 +243,8 @@ export class ValidationPool extends ReputationHolder {
|
|||
// Before evaluating the winning conditions,
|
||||
// we need to make sure any staked tokens are locked for the
|
||||
// specified amounts of time.
|
||||
for (const {
|
||||
tokenAddress, tokenTypeId, amount, lockingTime,
|
||||
} of this.stakes.values()) {
|
||||
this.dao.reputation.lock(tokenAddress, tokenTypeId, amount, lockingTime);
|
||||
for (const { tokenId, amount, lockingTime } of this.stakes.values()) {
|
||||
this.dao.reputation.lock(tokenId, amount, lockingTime);
|
||||
// TODO: If there is an exception here, the voter may have voted incorrectly. Consider penalties.
|
||||
}
|
||||
}
|
||||
|
@ -286,33 +261,23 @@ export class ValidationPool extends ReputationHolder {
|
|||
this.state = ValidationPoolStates.CLOSED;
|
||||
this.setStatus('Closed');
|
||||
|
||||
// Votes should be scaled by weights of this.reputationTypes
|
||||
const upvoteValue = this.reputationTypes.reduce((value, { reputationTypeId, weight }) => {
|
||||
value += this.getStakedAmount({ outcome: true, reputationTypeId }) * weight;
|
||||
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 upvoteValue = this.getTotalValueOfStakesForOutcome(true);
|
||||
const downvoteValue = this.getTotalValueOfStakesForOutcome(false);
|
||||
const activeAvailableReputation = this.dao.getActiveAvailableReputation();
|
||||
const votePasses = upvoteValue >= params.winningRatio * downvoteValue;
|
||||
const quorumMet = upvoteValue + downvoteValue >= params.quorum * activeAvailableReputation;
|
||||
|
||||
const result = {
|
||||
outcome,
|
||||
votePasses,
|
||||
upvoteValue,
|
||||
downvoteValue,
|
||||
};
|
||||
|
||||
if (quorumMet) {
|
||||
this.setStatus(`Resolved - ${outcome ? 'Won' : 'Lost'}`);
|
||||
this.scene?.sequence.log(`note over ${this.name} : ${outcome ? 'Win' : 'Lose'}`);
|
||||
this.setStatus(`Resolved - ${votePasses ? 'Won' : 'Lost'}`);
|
||||
this.scene?.sequence.log(`note over ${this.name} : ${votePasses ? 'Win' : 'Lose'}`);
|
||||
this.applyTokenLocking();
|
||||
await this.distributeReputation({ outcome });
|
||||
await this.distributeReputation({ votePasses });
|
||||
// TODO: distribute fees
|
||||
} else {
|
||||
this.setStatus('Resolved - Quorum not met');
|
||||
|
@ -336,57 +301,47 @@ export class ValidationPool extends ReputationHolder {
|
|||
return result;
|
||||
}
|
||||
|
||||
async distributeReputation({ outcome }) {
|
||||
// In a binding validation pool, losing voter stakes are transferred to winning voters.
|
||||
// TODO: Regression tests for different tokenLossRatio values
|
||||
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 });
|
||||
async distributeReputation({ votePasses }) {
|
||||
// For now we assume a tightly binding pool, where all staked reputation is lost
|
||||
// TODO: Take tokenLossRatio into account
|
||||
// TODO: revoke staked reputation from losing voters
|
||||
|
||||
// In a tightly binding validation pool, losing voter stakes are transferred to winning voters.
|
||||
const tokensForWinners = this.getTotalStakedOnPost(!votePasses);
|
||||
const winningEntries = this.getStakes(votePasses, { excludeSystem: true });
|
||||
const totalValueOfStakesForWin = this.getTotalValueOfStakesForOutcome(votePasses);
|
||||
|
||||
// Compute rewards for the winning voters, in proportion to the value of their stakes.
|
||||
for (const stake of winningEntries) {
|
||||
const { tokenAddress, amount } = stake;
|
||||
const value = stake.getAmount({ lockingTimeExponent: params.lockingTimeExponent });
|
||||
const { tokenId, amount } = stake;
|
||||
const value = stake.getStakeValue({ lockingTimeExponent: params.lockingTimeExponent });
|
||||
const reward = tokensForWinners * (value / totalValueOfStakesForWin);
|
||||
// Also return each winning voter their staked amount
|
||||
const reputationPublicKey = this.dao.reputation.ownerOf(tokenAddress);
|
||||
console.log(`reward of type ${reputationTypeId} for winning stake by ${reputationPublicKey}: ${reward}`);
|
||||
this.dao.reputation.transferValueFrom(this.tokenAddress, tokenAddress, reputationTypeId, reward + amount);
|
||||
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)} type ${reputationTypeId})`);
|
||||
}
|
||||
this.actions.reward.log(this, toActor, `(${displayNumber(reward)})`);
|
||||
}
|
||||
|
||||
if (outcome === true) {
|
||||
if (votePasses) {
|
||||
// Distribute awards to author via the forum
|
||||
const tokens = this.reputationTypes.reduce((values, { reputationTypeId }) => {
|
||||
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);
|
||||
// const tokensForAuthor = this.mintedValue * params.stakeForAuthor + rewards.get(this.tokenId);
|
||||
console.log(`sending reward for author stake to forum: ${this.dao.reputation.valueOf(this.tokenId)}`);
|
||||
|
||||
// Transfer ownership of the minted token, from the pool to the forum
|
||||
this.dao.reputation.transfer(this.id, this.dao.forum.id, this.tokenAddress);
|
||||
// const value = this.dao.reputation.valueOf(this.tokenAddress);
|
||||
this.dao.reputation.transfer(this.id, this.dao.forum.id, this.tokenId);
|
||||
// const value = this.dao.reputation.valueOf(this.tokenId);
|
||||
// this.actions.transfer.log(this, this.dao.forum, `(${value})`);
|
||||
|
||||
const result = {
|
||||
// Recurse through forum to determine reputation effects
|
||||
await this.dao.forum.onValidate({
|
||||
pool: this,
|
||||
postId: this.postId,
|
||||
tokenAddress: this.tokenAddress,
|
||||
tokenId: this.tokenId,
|
||||
referenceChainLimit: params.referenceChainLimit,
|
||||
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');
|
||||
|
|
|
@ -89,9 +89,6 @@ export class Actor {
|
|||
async computeDisplayValues(cb) {
|
||||
for (const [label, fn] of this.valueFunctions.entries()) {
|
||||
const value = fn();
|
||||
console.log('computeDisplay', {
|
||||
label, value, fn, cb,
|
||||
});
|
||||
await this.setDisplayValue(label, value);
|
||||
if (cb) {
|
||||
cb(label, value);
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { DisplayValue } from './display-value.js';
|
||||
import { randomID } from '../../util/helpers.js';
|
||||
import { Rectangle, Vector } from '../supporting/geometry/index.js';
|
||||
|
||||
export class Box {
|
||||
constructor(name, parentEl, options = {}) {
|
||||
|
@ -21,7 +20,6 @@ export class Box {
|
|||
parentEl.appendChild(this.el);
|
||||
}
|
||||
}
|
||||
this.position = options.position ?? Vector.zeros(2);
|
||||
}
|
||||
|
||||
flex({ center = false } = {}) {
|
||||
|
@ -37,6 +35,11 @@ export class Box {
|
|||
return this;
|
||||
}
|
||||
|
||||
hidden() {
|
||||
this.addClass('hidden');
|
||||
return this;
|
||||
}
|
||||
|
||||
addClass(className) {
|
||||
this.el.classList.add(className);
|
||||
return this;
|
||||
|
@ -60,19 +63,4 @@ export class Box {
|
|||
getId() {
|
||||
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,6 +37,7 @@ export class Document extends Box {
|
|||
}
|
||||
|
||||
get lastElement() {
|
||||
if (!this.elements.length) return null;
|
||||
return this.elements[this.elements.length - 1];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,354 +0,0 @@
|
|||
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,20 +19,6 @@ 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 {
|
||||
constructor(name, form, opts) {
|
||||
super(name, form, { ...opts, cbEventTypes: ['click'] });
|
||||
|
@ -57,18 +43,15 @@ export class TextField extends FormElement {
|
|||
constructor(name, form, opts) {
|
||||
super(name, form, opts);
|
||||
this.flex({ center: true });
|
||||
|
||||
// Place label inside a div, for improved styling
|
||||
const labelDiv = document.createElement('div');
|
||||
labelDiv.innerHTML = opts.label || name;
|
||||
const label = document.createElement('label');
|
||||
label.appendChild(labelDiv);
|
||||
const input = document.createElement('input');
|
||||
input.disabled = !!opts.disabled;
|
||||
input.defaultValue = opts.defaultValue || '';
|
||||
label.appendChild(input);
|
||||
this.el.appendChild(label);
|
||||
this.input = input;
|
||||
this.label = document.createElement('label');
|
||||
this.labelDiv = document.createElement('div');
|
||||
this.label.appendChild(this.labelDiv);
|
||||
this.labelDiv.innerHTML = opts.label || name;
|
||||
this.input = document.createElement('input');
|
||||
this.input.disabled = !!opts.disabled;
|
||||
this.input.defaultValue = opts.defaultValue || '';
|
||||
this.label.appendChild(this.input);
|
||||
this.el.appendChild(this.label);
|
||||
}
|
||||
|
||||
get value() {
|
||||
|
@ -78,30 +61,6 @@ export class TextField 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 {
|
||||
constructor(name, form, opts) {
|
||||
super(name, form, opts);
|
||||
|
@ -117,30 +76,36 @@ export class SubFormArray extends FormElement {
|
|||
this.subForms.splice(idx, 1);
|
||||
subForm.el.remove();
|
||||
}
|
||||
}
|
||||
|
||||
subForm(opts = {}) {
|
||||
const subForm = new SubForm(opts.name, this.form, { ...opts, subFormArray: this });
|
||||
this.subForms.push(subForm);
|
||||
return this;
|
||||
export class SubForm extends FormElement {
|
||||
constructor(name, form, opts) {
|
||||
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 lastSubForm() {
|
||||
return this.subForms[this.subForms.length - 1];
|
||||
get value() {
|
||||
return this.subForm.value;
|
||||
}
|
||||
}
|
||||
|
||||
export class FileInput extends FormElement {
|
||||
constructor(name, form, opts) {
|
||||
super(name, form, opts);
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'application/json';
|
||||
input.classList.add('visually-hidden');
|
||||
const label = document.createElement('label');
|
||||
const button = form.button({ name, cb: () => input.click() }).lastItem;
|
||||
label.appendChild(button.el);
|
||||
label.appendChild(input);
|
||||
this.el.appendChild(label);
|
||||
this.input = document.createElement('input');
|
||||
this.input.type = 'file';
|
||||
this.input.accept = 'application/json';
|
||||
this.input.classList.add('visually-hidden');
|
||||
this.label = document.createElement('label');
|
||||
this.button = form.button({ name, cb: () => this.input.click() }).lastItem;
|
||||
this.label.appendChild(this.button.el);
|
||||
this.label.appendChild(this.input);
|
||||
this.el.appendChild(this.label);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -154,11 +119,6 @@ export class Form extends Box {
|
|||
this.el.onsubmit = () => false;
|
||||
}
|
||||
|
||||
select(opts) {
|
||||
this.items.push(new Select(opts.name, this, opts));
|
||||
return this;
|
||||
}
|
||||
|
||||
button(opts) {
|
||||
this.items.push(new Button(opts.name, this, opts));
|
||||
return this;
|
||||
|
@ -194,10 +154,6 @@ export class Form extends Box {
|
|||
return this;
|
||||
}
|
||||
|
||||
remark(text, opts) {
|
||||
this.document.remark(text, { ...opts, parentEl: this.el });
|
||||
}
|
||||
|
||||
get lastItem() {
|
||||
return this.items[this.items.length - 1];
|
||||
}
|
||||
|
|
|
@ -1,65 +0,0 @@
|
|||
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) {
|
||||
this.name = 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.topSection = this.box.addBox('Top section').flex();
|
||||
this.displayValuesBox = this.topSection.addBox('Values');
|
||||
|
|
|
@ -1,164 +0,0 @@
|
|||
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,64 +1,56 @@
|
|||
import { ERC1155 } from '../supporting/erc1155.js';
|
||||
import { ERC721 } from '../supporting/erc721.js';
|
||||
import { randomID } from '../../util/helpers.js';
|
||||
import { EPSILON } from '../../util/constants.js';
|
||||
|
||||
class Lock {
|
||||
constructor(tokenAddress, tokenTypeId, amount, duration) {
|
||||
constructor(tokenId, amount, duration) {
|
||||
this.dateCreated = new Date();
|
||||
this.tokenAddress = tokenAddress;
|
||||
this.tokenId = tokenId;
|
||||
this.amount = amount;
|
||||
this.duration = duration;
|
||||
this.tokenTypeId = tokenTypeId;
|
||||
}
|
||||
}
|
||||
|
||||
export class ReputationTokenContract extends ERC1155 {
|
||||
export class ReputationTokenContract extends ERC721 {
|
||||
constructor() {
|
||||
super('Reputation', 'REP');
|
||||
this.histories = new Map(); // token address --> {tokenTypeId, increment, context (i.e. validation pool id)}
|
||||
this.values = new Map(); // token address --> token type id --> current value
|
||||
this.locks = new Set(); // {tokenAddress, tokenTypeId, amount, start, duration}
|
||||
this.histories = new Map(); // token id --> {increment, context (i.e. validation pool id)}
|
||||
this.values = new Map(); // token id --> current value
|
||||
this.locks = new Set(); // {tokenId, amount, start, duration}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param to Recipient address
|
||||
* @param values Object with reputation type id as key, and amount of reputation as value
|
||||
* @param to
|
||||
* @param value
|
||||
* @param context
|
||||
* @returns {string}
|
||||
*/
|
||||
mintBatch(to, tokenTypeIds, values) {
|
||||
const tokenAddress = `token_${randomID()}`;
|
||||
super.mintBatch(to, tokenAddress, tokenTypeIds, tokenTypeIds.map(() => 1));
|
||||
const tokenMap = new Map();
|
||||
for (let idx = 0; idx < tokenTypeIds.length; idx++) {
|
||||
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;
|
||||
mint(to, value, context = {}) {
|
||||
const tokenId = `token_${randomID()}`;
|
||||
super.mint(to, tokenId);
|
||||
this.values.set(tokenId, value);
|
||||
this.histories.set(tokenId, [{ increment: value, context }]);
|
||||
return tokenId;
|
||||
}
|
||||
|
||||
incrementValue(tokenAddress, tokenTypeId, increment, context) {
|
||||
const tokenTypeIds = this.values.get(tokenAddress);
|
||||
if (tokenTypeIds === undefined) {
|
||||
throw new Error(`Token not found: ${tokenAddress}`);
|
||||
incrementValue(tokenId, increment, context) {
|
||||
const value = this.values.get(tokenId);
|
||||
if (value === undefined) {
|
||||
throw new Error(`Token not found: ${tokenId}`);
|
||||
}
|
||||
const value = tokenTypeIds?.get(tokenTypeId);
|
||||
const newValue = value + increment;
|
||||
const history = this.histories.get(tokenAddress) || [];
|
||||
const history = this.histories.get(tokenId) || [];
|
||||
|
||||
if (newValue < -EPSILON) {
|
||||
throw new Error(`Token value can not become negative. Attempted to set value = ${newValue}`);
|
||||
}
|
||||
tokenTypeIds.set(tokenAddress, newValue);
|
||||
this.values.set(tokenAddress, tokenTypeIds);
|
||||
history.push({ tokenTypeId, increment, context });
|
||||
this.histories.set(tokenAddress, history);
|
||||
this.values.set(tokenId, newValue);
|
||||
history.push({ increment, context });
|
||||
this.histories.set(tokenId, history);
|
||||
}
|
||||
|
||||
transferValueFrom(from, to, tokenTypeId, amount) {
|
||||
transferValueFrom(fromTokenId, toTokenId, amount) {
|
||||
if (amount === undefined) {
|
||||
throw new Error('Transfer value: amount is undefined!');
|
||||
}
|
||||
|
@ -68,83 +60,64 @@ export class ReputationTokenContract extends ERC1155 {
|
|||
if (amount < 0) {
|
||||
throw new Error('Transfer value: amount must be positive');
|
||||
}
|
||||
const sourceAvailable = this.availableValueOf(from, tokenTypeId);
|
||||
const sourceAvailable = this.availableValueOf(fromTokenId);
|
||||
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);
|
||||
this.incrementValue(fromTokenId, -amount);
|
||||
this.incrementValue(toTokenId, amount);
|
||||
}
|
||||
|
||||
batchTransferValueFrom(from, to, tokenTypeIds, amounts) {
|
||||
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);
|
||||
lock(tokenId, amount, duration) {
|
||||
const lock = new Lock(tokenId, amount, duration);
|
||||
this.locks.add(lock);
|
||||
}
|
||||
|
||||
historyOf(tokenAddress) {
|
||||
return this.histories.get(tokenAddress);
|
||||
historyOf(tokenId) {
|
||||
return this.histories.get(tokenId);
|
||||
}
|
||||
|
||||
valueOf(tokenAddress, tokenTypeId) {
|
||||
const tokenTypeIds = this.values.get(tokenAddress);
|
||||
if (tokenTypeIds === undefined) {
|
||||
throw new Error(`Token not found: ${tokenAddress}`);
|
||||
valueOf(tokenId) {
|
||||
const value = this.values.get(tokenId);
|
||||
if (value === undefined) {
|
||||
throw new Error(`Token not found: ${tokenId}`);
|
||||
}
|
||||
return tokenTypeIds.get(tokenTypeId);
|
||||
return value;
|
||||
}
|
||||
|
||||
availableValueOf(tokenAddress, tokenTypeId) {
|
||||
availableValueOf(tokenId) {
|
||||
const amountLocked = Array.from(this.locks.values())
|
||||
.filter((lock) => lock.tokenAddress === tokenAddress && lock.tokenTypeId === tokenTypeId)
|
||||
.filter(({ tokenId: lockTokenId }) => lockTokenId === tokenId)
|
||||
.filter(({ dateCreated, duration }) => new Date() - dateCreated < duration)
|
||||
.reduce((total, { amount }) => total += amount, 0);
|
||||
|
||||
return this.valueOf(tokenAddress, tokenTypeId) - amountLocked;
|
||||
return this.valueOf(tokenId) - amountLocked;
|
||||
}
|
||||
|
||||
valueOwnedBy(ownerAddress, tokenTypeId) {
|
||||
valueOwnedBy(ownerId) {
|
||||
return Array.from(this.owners.entries())
|
||||
.filter(([__, owner]) => owner === ownerAddress)
|
||||
.map(([tokenAddress, __]) => this.valueOf(tokenAddress, tokenTypeId))
|
||||
.filter(([__, owner]) => owner === ownerId)
|
||||
.map(([tokenId, __]) => this.valueOf(tokenId))
|
||||
.reduce((total, value) => total += value, 0);
|
||||
}
|
||||
|
||||
availableValueOwnedBy(ownerAddress, tokenTypeId) {
|
||||
availableValueOwnedBy(ownerId) {
|
||||
return Array.from(this.owners.entries())
|
||||
.filter(([__, owner]) => owner === ownerAddress)
|
||||
.map(([tokenAddress, __]) => this.availableValueOf(tokenAddress, tokenTypeId))
|
||||
.filter(([__, owner]) => owner === ownerId)
|
||||
.map(([tokenId, __]) => this.availableValueOf(tokenId))
|
||||
.reduce((total, value) => total += value, 0);
|
||||
}
|
||||
|
||||
getTotal(tokenTypeId) {
|
||||
return Array.from(this.values.values())
|
||||
.flatMap((tokens) => tokens.get(tokenTypeId))
|
||||
.reduce((total, value) => total += value, 0);
|
||||
getTotal() {
|
||||
return Array.from(this.values.values()).reduce((total, value) => total += value, 0);
|
||||
}
|
||||
|
||||
// burn(tokenAddress, tokenTypeId, )
|
||||
getTotalAvailable() {
|
||||
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);
|
||||
}
|
||||
|
||||
getHTML() {
|
||||
getHtml() {
|
||||
const edges = this.getComorphicEdges();
|
||||
let html = '';
|
||||
html += '<table>';
|
||||
|
@ -49,18 +49,22 @@ export class Edge {
|
|||
return html;
|
||||
}
|
||||
|
||||
getFlowchartNode() {
|
||||
return `${Edge.getCombinedKey(this)}("${this.getHtml()}")`;
|
||||
}
|
||||
|
||||
displayEdgeNode() {
|
||||
if (this.options.hide) {
|
||||
return;
|
||||
}
|
||||
this.graph.flowchart?.log(this.getHTML());
|
||||
this.graph.flowchart?.log(this.getFlowchartNode());
|
||||
}
|
||||
|
||||
displayEdge() {
|
||||
if (this.options.hide) {
|
||||
return;
|
||||
}
|
||||
this.graph.flowchart?.log(`${this.from.id} --- ${this.getHTML()} --> ${this.to.id}`);
|
||||
this.graph.flowchart?.log(`${this.from.id} --- ${this.getFlowchartNode()} --> ${this.to.id}`);
|
||||
this.graph.flowchart?.log(`class ${Edge.getCombinedKey(this)} edge`);
|
||||
if (this.graph.editable && !this.installedClickCallback) {
|
||||
this.graph.flowchart?.log(`click ${Edge.getCombinedKey(this)} WDGHandler${this.graph.index} \
|
||||
|
@ -71,12 +75,6 @@ export class Edge {
|
|||
|
||||
static prepareEditorDocument(graph, doc, from, to) {
|
||||
const form = doc.form({ name: 'editorForm' }).lastElement;
|
||||
form.button({
|
||||
name: 'New Vertex',
|
||||
cb: () => {
|
||||
graph.resetEditor();
|
||||
},
|
||||
});
|
||||
doc.remark('<h3>Edit Edge</h3>', { parentEl: form.el });
|
||||
form
|
||||
.textField({
|
||||
|
@ -90,7 +88,7 @@ export class Edge {
|
|||
const subFormArray = form.subFormArray({ id: 'edges', name: 'edges' }).lastItem;
|
||||
|
||||
const addEdgeForm = (edge) => {
|
||||
const { subForm } = form.subForm({ subFormArray }).lastItem;
|
||||
const { subForm } = form.subForm({ name: 'subform', subFormArray }).lastItem;
|
||||
subForm.textField({
|
||||
id: 'type', name: 'type', defaultValue: edge.type, required: true,
|
||||
})
|
||||
|
|
|
@ -1,105 +0,0 @@
|
|||
/**
|
||||
* 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.owners = new Map(); // token id --> owner address
|
||||
// this.tokenApprovals = new Map(); // token id --> approved addresses
|
||||
// this.operatorApprovals = new Map(); // ownerAddress --> operator approvals
|
||||
// this.operatorApprovals = new Map(); // owner --> operator approvals
|
||||
|
||||
this.events = {
|
||||
// Transfer: (_from, _to, _tokenAddress) => {},
|
||||
// Approval: (_owner, _approved, _tokenAddress) => {},
|
||||
// Transfer: (_from, _to, _tokenId) => {},
|
||||
// Approval: (_owner, _approved, _tokenId) => {},
|
||||
// ApprovalForAll: (_owner, _operator, _approved) => {},
|
||||
};
|
||||
}
|
||||
|
||||
incrementBalance(ownerAddress, increment) {
|
||||
const balance = this.balances.get(ownerAddress) ?? 0;
|
||||
this.balances.set(ownerAddress, balance + increment);
|
||||
incrementBalance(owner, increment) {
|
||||
const balance = this.balances.get(owner) ?? 0;
|
||||
this.balances.set(owner, balance + increment);
|
||||
}
|
||||
|
||||
mint(to, tokenAddress) {
|
||||
console.log('ERC721.mint', { to, tokenAddress });
|
||||
if (this.owners.get(tokenAddress)) {
|
||||
mint(to, tokenId) {
|
||||
console.log('ERC721.mint', { to, tokenId });
|
||||
if (this.owners.get(tokenId)) {
|
||||
throw new Error('ERC721: token already minted');
|
||||
}
|
||||
this.incrementBalance(to, 1);
|
||||
this.owners.set(tokenAddress, to);
|
||||
this.owners.set(tokenId, to);
|
||||
}
|
||||
|
||||
burn(tokenAddress) {
|
||||
const ownerAddress = this.owners.get(tokenAddress);
|
||||
this.incrementBalance(ownerAddress, -1);
|
||||
this.owners.delete(tokenAddress);
|
||||
burn(tokenId) {
|
||||
const owner = this.owners.get(tokenId);
|
||||
this.incrementBalance(owner, -1);
|
||||
this.owners.delete(tokenId);
|
||||
}
|
||||
|
||||
balanceOf(ownerAddress) {
|
||||
if (!ownerAddress) {
|
||||
balanceOf(owner) {
|
||||
if (!owner) {
|
||||
throw new Error('ERC721: address zero is not a valid owner');
|
||||
}
|
||||
return this.balances.get(ownerAddress) ?? 0;
|
||||
return this.balances.get(owner) ?? 0;
|
||||
}
|
||||
|
||||
ownerOf(tokenAddress) {
|
||||
const ownerAddress = this.owners.get(tokenAddress);
|
||||
if (!ownerAddress) {
|
||||
throw new Error(`ERC721: invalid token ID: ${tokenAddress}`);
|
||||
ownerOf(tokenId) {
|
||||
const owner = this.owners.get(tokenId);
|
||||
if (!owner) {
|
||||
throw new Error(`ERC721: invalid token ID: ${tokenId}`);
|
||||
}
|
||||
return ownerAddress;
|
||||
return owner;
|
||||
}
|
||||
|
||||
transfer(from, to, tokenAddress) {
|
||||
console.log('ERC721.transfer', { from, to, tokenAddress });
|
||||
const ownerAddress = this.owners.get(tokenAddress);
|
||||
if (ownerAddress !== from) {
|
||||
throw new Error(`ERC721: transfer from incorrect owner ${from}; should be ${ownerAddress}`);
|
||||
transfer(from, to, tokenId) {
|
||||
console.log('ERC721.transfer', { from, to, tokenId });
|
||||
const owner = this.owners.get(tokenId);
|
||||
if (owner !== from) {
|
||||
throw new Error(`ERC721: transfer from incorrect owner ${from}; should be ${owner}`);
|
||||
}
|
||||
this.incrementBalance(from, -1);
|
||||
this.incrementBalance(to, 1);
|
||||
this.owners.set(tokenAddress, to);
|
||||
this.owners.set(tokenId, 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.
|
||||
/// multiple operators per owner.
|
||||
/// @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
|
||||
/// @dev Throws if `_tokenId` is not a valid NFT.
|
||||
/// @param _tokenId 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) {}
|
||||
// getApproved(_tokenId) {}
|
||||
|
||||
/// @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
|
||||
/// @param _operator The address that acts on behalf of the owner
|
||||
/// @return True if `_operator` is an approved operator for `_owner`, false otherwise
|
||||
// isApprovedForAll(_owner, _operator) {}
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
export * from './vector.js';
|
||||
export * from './polygon.js';
|
||||
export * from './rectangle.js';
|
|
@ -1,18 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
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,15 +1,14 @@
|
|||
export class Stake {
|
||||
constructor({
|
||||
tokenAddress, tokenTypeId, position, amount, lockingTime,
|
||||
tokenId, position, amount, lockingTime,
|
||||
}) {
|
||||
this.tokenAddress = tokenAddress;
|
||||
this.tokenId = tokenId;
|
||||
this.position = position;
|
||||
this.amount = amount;
|
||||
this.lockingTime = lockingTime;
|
||||
this.tokenTypeId = tokenTypeId;
|
||||
}
|
||||
|
||||
getAmount({ lockingTimeExponent } = {}) {
|
||||
getStakeValue({ lockingTimeExponent } = {}) {
|
||||
return this.amount * this.lockingTime ** lockingTimeExponent;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ export class Vertex {
|
|||
to: [],
|
||||
};
|
||||
this.installedClickCallback = false;
|
||||
this.properties = options.properties ?? new Map();
|
||||
this.properties = new Map();
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
|
@ -40,37 +40,29 @@ export class Vertex {
|
|||
);
|
||||
}
|
||||
|
||||
async setProperty(key, value) {
|
||||
setProperty(key, value) {
|
||||
this.properties.set(key, value);
|
||||
await this.onUpdate?.();
|
||||
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() {
|
||||
if (this.options.hide) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.graph.flowchart?.log(`${this.id}["${this.getHTML()}"]`);
|
||||
let html = '';
|
||||
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) {
|
||||
this.graph.flowchart?.log(`click ${this.id} WDGHandler${this.graph.index} "Edit Vertex ${this.id}"`);
|
||||
|
@ -81,34 +73,26 @@ export class Vertex {
|
|||
static prepareEditorDocument(graph, doc, vertexId) {
|
||||
const vertex = vertexId ? graph.getVertex(vertexId) : undefined;
|
||||
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 });
|
||||
form
|
||||
.textField({
|
||||
name: 'id', defaultValue: vertex?.id,
|
||||
id: 'id', name: 'id', defaultValue: vertex?.id,
|
||||
})
|
||||
.textField({ name: 'type', defaultValue: vertex?.type })
|
||||
.textField({ name: 'label', defaultValue: vertex?.label });
|
||||
.textField({ id: 'type', name: 'type', defaultValue: vertex?.type })
|
||||
.textField({ id: 'label', name: 'label', defaultValue: vertex?.label });
|
||||
|
||||
doc.remark('<h4>Properties</h4>', { parentEl: form.el });
|
||||
const subFormArray = form.subFormArray({ name: 'properties' }).lastItem;
|
||||
const subFormArray = form.subFormArray({ id: 'properties', name: 'properties' }).lastItem;
|
||||
const addPropertyForm = (key, value) => {
|
||||
const { subForm } = form.subForm({ subFormArray }).lastItem;
|
||||
subForm.textField({ name: 'key', defaultValue: key })
|
||||
.textField({ name: 'value', defaultValue: value })
|
||||
const { subForm } = form.subForm({ name: 'subform', subFormArray }).lastItem;
|
||||
subForm.textField({ id: 'key', name: 'key', defaultValue: key })
|
||||
.textField({ id: 'value', name: 'value', defaultValue: value })
|
||||
.button({
|
||||
id: 'remove',
|
||||
name: 'Remove Property',
|
||||
cb: () => subFormArray.remove(subForm),
|
||||
})
|
||||
.remark('<br>');
|
||||
});
|
||||
doc.remark('<br>', { parentEl: subForm.el });
|
||||
};
|
||||
|
||||
if (vertex) {
|
||||
|
@ -118,11 +102,13 @@ export class Vertex {
|
|||
}
|
||||
|
||||
form.button({
|
||||
id: 'add',
|
||||
name: 'Add Property',
|
||||
cb: () => addPropertyForm('', ''),
|
||||
});
|
||||
|
||||
form.submit({
|
||||
id: 'save',
|
||||
name: 'Save',
|
||||
cb: ({ form: { value: formValue } }) => {
|
||||
let fullRedraw = false;
|
||||
|
@ -135,10 +121,7 @@ export class Vertex {
|
|||
Object.assign(vertex, formValue);
|
||||
vertex.displayVertex();
|
||||
} else {
|
||||
const {
|
||||
type, id, label, properties,
|
||||
} = formValue;
|
||||
const newVertex = graph.addVertex(type, id, null, label, { properties });
|
||||
const newVertex = graph.addVertex(formValue.type, formValue.id, null, formValue.label);
|
||||
Object.assign(newVertex, formValue);
|
||||
doc.clear();
|
||||
Vertex.prepareEditorDocument(graph, doc, newVertex.id);
|
||||
|
@ -151,6 +134,7 @@ export class Vertex {
|
|||
|
||||
if (vertex) {
|
||||
form.button({
|
||||
id: 'delete',
|
||||
name: 'Delete Vertex',
|
||||
cb: () => {
|
||||
graph.deleteVertex(vertex.id);
|
||||
|
@ -175,6 +159,7 @@ export class Vertex {
|
|||
}
|
||||
|
||||
form.button({
|
||||
id: 'cancel',
|
||||
name: 'Cancel',
|
||||
cb: () => graph.resetEditor(),
|
||||
parentEl: doc.el,
|
||||
|
|
|
@ -1,12 +1,38 @@
|
|||
import { Vertex } from './vertex.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 {
|
||||
constructor(name) {
|
||||
constructor(name, scene, options = {}) {
|
||||
this.name = name;
|
||||
this.scene = scene;
|
||||
this.vertices = new Map();
|
||||
this.edgeTypes = new Map();
|
||||
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
|
||||
this.history = [];
|
||||
|
@ -46,9 +72,108 @@ export class WeightedDirectedGraph {
|
|||
} of edges) {
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
// Supports simple case of auto-incremented numeric ids
|
||||
if (typeof id === 'object') {
|
||||
|
@ -61,6 +186,7 @@ export class WeightedDirectedGraph {
|
|||
}
|
||||
const vertex = new Vertex(this, type, id, data, { ...options, label });
|
||||
this.vertices.set(id, vertex);
|
||||
vertex.displayVertex();
|
||||
return vertex;
|
||||
}
|
||||
|
||||
|
@ -103,6 +229,7 @@ export class WeightedDirectedGraph {
|
|||
}
|
||||
const edgeKey = Edge.getKey(edge);
|
||||
edges.set(edgeKey, edge);
|
||||
edge.displayEdgeNode();
|
||||
return edge;
|
||||
}
|
||||
|
||||
|
@ -112,9 +239,15 @@ export class WeightedDirectedGraph {
|
|||
if (this.getEdge(type, from, to)) {
|
||||
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);
|
||||
from.edges.from.push(edge);
|
||||
to.edges.to.push(edge);
|
||||
if (existingEdges.length) {
|
||||
edge.displayEdgeNode();
|
||||
} else {
|
||||
edge.displayEdge();
|
||||
}
|
||||
return edge;
|
||||
}
|
||||
|
||||
|
|
|
@ -50,12 +50,6 @@ a:visited {
|
|||
left: 12em;
|
||||
top: -0.5em;
|
||||
}
|
||||
.fixed {
|
||||
position: fixed;
|
||||
}
|
||||
.absolute {
|
||||
position: absolute;
|
||||
}
|
||||
svg {
|
||||
width: 800px;
|
||||
}
|
||||
|
@ -113,21 +107,3 @@ span.small {
|
|||
white-space: nowrap;
|
||||
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,19 +15,18 @@
|
|||
For more information please see the <a href="https://daogovernanceframework.com/wiki/DAO_Governance_Framework">DGF
|
||||
Wiki</a>.
|
||||
</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>
|
||||
<ul>
|
||||
<li><a href="./wdg-editor">Weighted Directed Graph Editor</a></li>
|
||||
<li><a href="./schema-editor">Schema Editor</a></li>
|
||||
</ul>
|
||||
<h2>Example Scenarios</h2>
|
||||
<p>
|
||||
Below are example scenarios with various assertions covering features of our reputation system.
|
||||
</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>
|
||||
<li><a href="./tests/validation-pool.test.html">Validation Pool</a></li>
|
||||
<li><a href="./tests/availability.test.html">Availability + Business</a></li>
|
||||
|
@ -67,7 +66,6 @@
|
|||
<li><a href="./tests/mocha.test.html">Mocha</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/force-directed.test.html">Force-Directed Graph</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<h4><a href="./tests/all.test.html">All</a></h4>
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
<!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>
|
|
@ -1,9 +0,0 @@
|
|||
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,7 +40,6 @@
|
|||
<script type="module" src="./scripts/forum/forum11.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/force-directed.test.js"></script>
|
||||
<script defer class="mocha-init">
|
||||
mocha.setup({
|
||||
ui: 'bdd',
|
||||
|
|
|
@ -27,6 +27,6 @@
|
|||
<script defer class="mocha-init">
|
||||
mocha.setup({
|
||||
ui: 'bdd',
|
||||
});
|
||||
});
|
||||
window.should = chai.should();
|
||||
</script>
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
<!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,7 +6,6 @@ import { Public } from '../../classes/actors/public.js';
|
|||
import { PostContent } from '../../classes/supporting/post-content.js';
|
||||
import { delayOrWait } from '../../classes/display/scene-controls.js';
|
||||
import { mochaRun } from '../../util/helpers.js';
|
||||
import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../util/constants.js';
|
||||
|
||||
const DELAY_INTERVAL = 100;
|
||||
const POOL_DURATION = 200;
|
||||
|
@ -32,8 +31,8 @@ const setup = async () => {
|
|||
scene.withFlowchart();
|
||||
scene.withTable();
|
||||
|
||||
dao = new DAO('DGF', scene);
|
||||
await dao.setDisplayValue('total rep', () => dao.reputation.getTotal(DEFAULT_REP_TOKEN_TYPE_ID));
|
||||
dao = new DAO('DAO', scene);
|
||||
await dao.setDisplayValue('total rep', () => dao.reputation.getTotal());
|
||||
|
||||
experts = [];
|
||||
|
||||
|
@ -59,7 +58,7 @@ const setup = async () => {
|
|||
await pool1.evaluateWinningConditions();
|
||||
await delayOrWait(DELAY_INTERVAL);
|
||||
|
||||
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(10);
|
||||
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(10);
|
||||
|
||||
const { pool: pool2 } = await experts[1].submitPostWithFee(
|
||||
new PostContent({ hello: 'to you as well' })
|
||||
|
@ -78,8 +77,8 @@ const setup = async () => {
|
|||
await pool2.evaluateWinningConditions();
|
||||
await delayOrWait(DELAY_INTERVAL);
|
||||
|
||||
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(15);
|
||||
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
|
||||
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(15);
|
||||
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(5);
|
||||
};
|
||||
|
||||
const getActiveWorker = async () => {
|
||||
|
|
|
@ -1,204 +0,0 @@
|
|||
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,7 +4,6 @@ import { Expert } from '../../classes/actors/expert.js';
|
|||
import { PostContent } from '../../classes/supporting/post-content.js';
|
||||
import { DAO } from '../../classes/dao/dao.js';
|
||||
import { delayOrWait } from '../../classes/display/scene-controls.js';
|
||||
import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../util/constants.js';
|
||||
|
||||
export class ForumTest {
|
||||
constructor(options) {
|
||||
|
@ -87,6 +86,6 @@ export class ForumTest {
|
|||
|
||||
await this.newExpert();
|
||||
|
||||
await this.dao.addComputedValue('total rep', () => this.dao.reputation.getTotal(DEFAULT_REP_TOKEN_TYPE_ID));
|
||||
await this.dao.addComputedValue('total value', () => this.dao.reputation.getTotal());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../../util/constants.js';
|
||||
import { mochaRun } from '../../../util/helpers.js';
|
||||
import { ForumTest } from '../forum.test-util.js';
|
||||
|
||||
|
@ -35,9 +34,9 @@ describe('Forum', function tests() {
|
|||
await forumTest.addPost(authors, 10);
|
||||
forum.getPost(posts[0]).value.should.equal(10);
|
||||
|
||||
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
|
||||
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(2.5);
|
||||
dao.reputation.valueOwnedBy(experts[2].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(2.5);
|
||||
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(5);
|
||||
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(2.5);
|
||||
dao.reputation.valueOwnedBy(experts[2].reputationPublicKey).should.equal(2.5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { DEFAULT_REP_TOKEN_TYPE_ID } from '../../../util/constants.js';
|
||||
import { mochaRun } from '../../../util/helpers.js';
|
||||
import { ForumTest } from '../forum.test-util.js';
|
||||
|
||||
|
@ -34,9 +33,9 @@ describe('Forum', function tests() {
|
|||
await forumTest.addPost(authors, 10);
|
||||
forum.getPost(posts[0]).value.should.equal(10);
|
||||
|
||||
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
|
||||
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
|
||||
dao.reputation.valueOwnedBy(experts[2].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(0);
|
||||
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(5);
|
||||
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(5);
|
||||
dao.reputation.valueOwnedBy(experts[2].reputationPublicKey).should.equal(0);
|
||||
});
|
||||
|
||||
it('Post2 with two authors, one shared with Post1', async () => {
|
||||
|
@ -47,9 +46,9 @@ describe('Forum', function tests() {
|
|||
await forumTest.addPost(authors, 10);
|
||||
forum.getPost(posts[0]).value.should.equal(10);
|
||||
|
||||
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
|
||||
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(10);
|
||||
dao.reputation.valueOwnedBy(experts[2].reputationPublicKey, DEFAULT_REP_TOKEN_TYPE_ID).should.equal(5);
|
||||
dao.reputation.valueOwnedBy(experts[0].reputationPublicKey).should.equal(5);
|
||||
dao.reputation.valueOwnedBy(experts[1].reputationPublicKey).should.equal(10);
|
||||
dao.reputation.valueOwnedBy(experts[2].reputationPublicKey).should.equal(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -29,7 +29,7 @@ async function setup() {
|
|||
scene.withSequenceDiagram();
|
||||
scene.withTable();
|
||||
|
||||
dao = new DAO('DGF', scene);
|
||||
dao = new DAO('DAO', scene);
|
||||
|
||||
experts = [];
|
||||
await newExpert();
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { Box } from '../../classes/display/box.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 { mochaRun } from '../../util/helpers.js';
|
||||
|
||||
|
@ -14,7 +13,7 @@ describe('Weighted Directed Graph', function tests() {
|
|||
let graph;
|
||||
|
||||
before(() => {
|
||||
graph = (window.graph = new WDGDiagram('test1', window.scene)).withFlowchart();
|
||||
graph = (window.graph = new WeightedDirectedGraph('test1', window.scene)).withFlowchart();
|
||||
|
||||
graph.addVertex('v1', {});
|
||||
graph.addVertex('v1', {});
|
||||
|
@ -151,4 +150,20 @@ 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();
|
||||
|
|
|
@ -1,28 +1,14 @@
|
|||
export const EPSILON = 2.23e-16;
|
||||
|
||||
export const INCINERATOR_ADDRESS = '0';
|
||||
|
||||
export const EdgeTypes = {
|
||||
CITATION: 'citation',
|
||||
BALANCE: 'balance',
|
||||
AUTHOR: 'author',
|
||||
};
|
||||
|
||||
export const VertexTypes = {
|
||||
POST: 'post',
|
||||
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 { Scene } from '../classes/display/scene.js';
|
||||
import { WDGDiagram } from '../classes/display/wdg-mermaid-ui.js';
|
||||
import { WeightedDirectedGraph } from '../classes/supporting/wdg.js';
|
||||
|
||||
const rootElement = document.getElementById('scene');
|
||||
const rootBox = new Box('rootBox', rootElement).flex();
|
||||
window.disableSceneControls = true;
|
||||
window.scene = new Scene('WDG Editor', rootBox);
|
||||
window.graph = new WDGDiagram('new', window.scene, { editable: true }).withFlowchart();
|
||||
window.graph = new WeightedDirectedGraph('new', window.scene, { editable: true }).withFlowchart();
|
||||
|
|
Loading…
Reference in New Issue