import { BoundingBox, Vector2, Transformation, svgTransformationDecode, svgTransformationEncode, } from 'touchcontroller'; import { IPlaygroundData, defaultPlaygroundData, } from '../interfaces/IPlaygroundData'; import { SNAP_DISTANCE } from './../config'; import { IPlaygroundDataInteraction, IPlaygroundDataInteractionRotate, } from './../interfaces/IPlaygroundData'; import { AttributeData } from './../tools/AttributeData'; import { Acceptor } from './Acceptor'; import { Donor } from './Donor'; import { PlaygroundActive } from './PlaygroundActive'; export class Rect extends BoundingBox { private _anchorPairs: { donor: Donor; acceptor: Acceptor }[] = []; public hovered = false; public playgroundData: IPlaygroundData; constructor( public color: string, public svgGroupElement: SVGGElement, center: Vector2, size: Vector2, rotation: number, public playgroundActive: PlaygroundActive, ) { super(center, size, rotation); this.playgroundData = new AttributeData( svgGroupElement, 'data-playground', defaultPlaygroundData(), ).data; //console.log(svgElement,this.playgroundData); this._initializeAnchorPoints(); this.startTransformations(true); } visualFeedback() { this.svgGroupElement.style.opacity = '0.5'; window.setTimeout(() => { this.svgGroupElement.style.opacity = '1'; }, 500); } public acceptors: Acceptor[]; startTransformations(initialize = false) { this.endTransformations(); this.acceptors = []; this.release(); //console.log(this); for (const rect of this.playgroundActive.rects) { if (rect !== this) { this.acceptors.push( ...rect.anchors.acceptors.filter( (acceptor) => !acceptor.full, ), ); } } } public shadowBoundingBox: BoundingBox; endTransformations() { //console.log('endTransformations'); this.lastSnapTransformation = Transformation.Neutral(); this.shadowBoundingBox = this.cloneDeep(); } private lastSnapTransformation: Transformation = Transformation.Neutral(); private getInteraction( type: 'drag' | 'rotate' | 'delete', ): IPlaygroundDataInteraction | null { return ( this.playgroundData.interactions.find( (interaction) => interaction.type === type, ) || null ); } private get rotateInteraction(): IPlaygroundDataInteractionRotate | null { return this.getInteraction( 'rotate', ) as IPlaygroundDataInteractionRotate | null; } public tap() { if (this.rotateInteraction) { this.applyTransformation( Transformation.rotate( (this.rotateInteraction.step / 180) * Math.PI, ), ); } } applyTransformation( transformation: Transformation, leadingIteration = 0, leader: Rect | null = null, ) { if (leader === this) { return; } else if (!leader) { leader = this; } if (leadingIteration > 10) { console.warn(`Chain was cancelled in 10. iteration.`); return; } if (!this.getInteraction('drag')) { transformation.translate = Vector2.Zero(); } if (!this.getInteraction('rotate')) { //transformation.rotate = 0; } transformation.scale = 1; this.shadowBoundingBox.applyTransformation(transformation); const snappedBoundingBox = this.snap(this.shadowBoundingBox); this.center = snappedBoundingBox.center; this.rotation = snappedBoundingBox.rotation; const snapTransformation = this.shadowBoundingBox.countTransformation( snappedBoundingBox, ); let transform = svgTransformationDecode( //todo const this.svgGroupElement.getAttribute('transform') || undefined, ); transform = transform //.add(Transformation.translate(new Vector2(-100,0))) .add(transformation) .subtract(this.lastSnapTransformation) .add(snapTransformation); //todo addInPlace this.lastSnapTransformation = snapTransformation; this.svgGroupElement.setAttribute( 'transform', svgTransformationEncode(transform), ); /* todo for (const acceptor of this.anchors .acceptors /*.filter((acceptor)=>acceptor.parent)* /) { for (const donor of acceptor.followingDonors) { donor.rect.applyTransformation(transformation, leadingIteration+1, leader); } } */ this.rawAnchorsWithoutRotationToAnchors(); } release() { for (const donor of this.anchors.donors) { donor.release(); } } snap(originalBoundingBox: BoundingBox) { //todo bounding box without size this.release(); this._anchorPairs = []; const donors = this.anchors.donors; const acceptors = this.acceptors; //-----------------phase 1 let targetPair1: { distance: number; donor: Donor; acceptor: Acceptor; } | null = null; for (const donor of donors) { //todo optimize for (const acceptor of acceptors.filter((acceptor) => acceptor.isAccepting(donor), )) { const distance = donor.position.length(acceptor.position); if (distance < SNAP_DISTANCE) { if (!targetPair1) { targetPair1 = { distance, donor, acceptor }; } else { if (distance < targetPair1.distance) { targetPair1 = { distance, donor, acceptor }; //todo DRY } } } } } if (!targetPair1) { return originalBoundingBox; } else { this._anchorPairs.push(targetPair1); //console.log(targetPair1); const translation = targetPair1.acceptor.position.subtract( targetPair1.donor.position, ); const snappedBoundingBox = originalBoundingBox.cloneDeep(); snappedBoundingBox.applyTransformation( Transformation.translate(translation), ); targetPair1.acceptor.snapDonor(targetPair1.donor); return snappedBoundingBox; //-----------------phase 2 /*/ todo const otherDonors = donors .filter((donor)=>donor!==targetPair1.donor) .map((donor)=>({ distance: donor.position.length(targetPair1.donor.position), original: donor })); const otherAcceptors = acceptors .filter((acceptor)=>acceptor!==targetPair1.acceptor) .map((acceptor)=>({ distance: acceptor.position.length(targetPair1.acceptor.position), original: acceptor })); let targetPair2 = null; for(const otherDonor of otherDonors){ for(const otherAcceptor of otherAcceptors){ if( Math.abs(otherDonor.original.distance - otherAcceptor.original.distance)<5 ){ const distance = otherDonor.original.position.length(otherAcceptor.original.position); if(distance<100){ if(!targetPair2){ targetPair2 = {distance,donor:otherDonor.original,acceptor:otherAcceptor.original}; }else{ if(distance { this.renderAnchor( ctx, anchorPair.donor.position, 'DONOR', index.toString(), ); //todo !!! this.renderAnchor( ctx, anchorPair.acceptor.position, 'ACCEPTOR', index.toString(), ); }); */ //this._afterRender = []; } renderBoundingBox( ctx: CanvasRenderingContext2D, boundingBox: BoundingBox, color: string, hovered: boolean, offset: Vector2, ) { ctx.save(); ctx.fillStyle = color; ctx.beginPath(); ctx.rotate(boundingBox.rotation); const translate = new Vector2( boundingBox.center.x, boundingBox.center.y, ) .rotate(-boundingBox.rotation) .subtractInPlace( new Vector2(boundingBox.size.x / 2, boundingBox.size.y / 2), ) .addInPlace(offset); ctx.translate(translate.x, translate.y); //todo with toArray ctx.rect(0, 0, boundingBox.size.x, boundingBox.size.y); ctx.fill(); /*{ ctx.fillStyle = '#ff0000'; ctx.lineWidth = 3; ctx.rect( -5,-5, 10,10); ctx.fill(); ctx.stroke(); }*/ if (hovered) { ctx.strokeStyle = '#000000'; ctx.lineWidth = 3; ctx.stroke(); } ctx.restore(); } renderAnchors(ctx: CanvasRenderingContext2D, offset: Vector2) { ctx.lineWidth = 1; { const size = 15; for (const anchor of this.anchors /*Visible*/.acceptors) { //todo optimize this.renderAnchor( ctx, anchor.position.add(offset), 'ACCEPTOR', `${anchor.donors.length}/${anchor.accepts}`, ); } } { const size = 10; for (const anchor of this.anchors /*Visible*/.donors) { //todo optimize this.renderAnchor(ctx, anchor.position.add(offset), 'DONOR'); } } } renderAnchor( ctx: CanvasRenderingContext2D, point: Vector2, type: string, label = '', ) { //todo as param only anchor object let size = 0; if (type === 'DONOR') { size = 10; } else if (type === 'ACCEPTOR') { size = 15; } ctx.rect(point.x - size / 2, point.y - size / 2, size, size); ctx.stroke(); if (label) { ctx.font = '20px Arial'; ctx.fillText(label, point.x - 5, point.y + 25); } } /*get anchors(){ return this._anchors(); } get anchorsVisible(){ return this._anchors(this); }*/ public anchors: { acceptors: Acceptor[]; donors: Donor[]; }; /* private rotateAnchor(anchor: Donor | Acceptor): Donor | Acceptor { //todo better typing - generic anchor = anchor.clone(); anchor.relativePosition = anchor.relativePosition.rotate(this.rotation); return anchor; } */ //todo purge and delete private rawAnchorsWithoutRotationToAnchors() { this.anchors = this.rawAnchorsWithoutRotation; /* todo purge this.anchors = { acceptors: this.rawAnchorsWithoutRotation.acceptors.map( (anchor) => this.rotateAnchor(anchor) as Acceptor, ), donors: this.rawAnchorsWithoutRotation.donors.map( (anchor) => this.rotateAnchor(anchor) as Donor, ), };*/ } private rawAnchorsWithoutRotation: { acceptors: Acceptor[]; donors: Donor[]; }; _initializeAnchorPoints() { const donors = this.playgroundData.anchors.donors.map( (donorConfig) => new Donor( this, donorConfig.type, new Vector2(donorConfig.position.x, donorConfig.position.y), donorConfig.follow, ), ); const acceptors = this.playgroundData.anchors.acceptors.map( (acceptorConfig) => new Acceptor( this, acceptorConfig.type, new Vector2( acceptorConfig.position.x, acceptorConfig.position.y, ), acceptorConfig.accepts, acceptorConfig.order, ), ); this.rawAnchorsWithoutRotation = { acceptors, donors, }; this.rawAnchorsWithoutRotationToAnchors(); } }