import { EventManager, svgTransformationDecode, svgTransformationEncode, Transformation, Vector2, } from 'touchcontroller'; import { v4 as uuidV4 } from 'uuid'; import { forImmediate, forValueDefined } from 'waitasecond'; import { defaultPlaygroundData, IPlaygroundData, } from '../interfaces/IPlaygroundData'; import { createPlaygroundStyles } from '../tools/createPlaygroundStyles'; import { getParentDeep, getParentDeepAttribute, } from '../tools/domElementDeepTools'; import { getBoundingRectWithoutRotation } from '../tools/getBoundingRectWithoutRotation'; import { getScroll } from '../tools/getScroll'; import { AttributeData } from './../tools/AttributeData'; import { PlaygroundActive } from './PlaygroundActive'; import { Rect } from './Rect'; //import { translateToVector, vectorToTranslate } from './svgTools'; interface IPlaygroundState { itemsIds: string[]; activeItems: Rect[]; } export class Playground { // TODO: initialized private itemsIds: string[] = []; private playgroundActive?: PlaygroundActive; private playgroundSvg: SVGElement; private playgroundSvgOffset: Vector2; private eventManager: EventManager = new EventManager(); private subscribers: Function[] = []; constructor( private playgroundElement: HTMLElement, private debugMode = false, private preserveOriginalSvg = false, ) { if (!playgroundElement) { throw new Error( `Can not initialize playground because playground element is null.`, ); } this.init(); } public subscribe(callback: Function) { this.subscribers.push(callback); } get state(): IPlaygroundState { return { itemsIds: this.itemsIds, activeItems: this.playgroundActive ? this.playgroundActive.rects : [], }; } public async addListeners(playgroundElement?: HTMLElement) { // console.log('Playground.addListeners'); for (const svgElement of Array.from( (playgroundElement || this.playgroundElement).querySelectorAll( 'svg', ), )) { const moveBy = Vector2.Zero(); if (svgElement.hasAttribute('viewBox')) { const [minX, minY, width, height] = svgElement .getAttribute('viewBox')! .split(' ') .map((s) => parseFloat(s)); moveBy.addInPlace(new Vector2(minX, minY)); } for (const gElement of Array.from( //todo rename gElementxxx svgElement.querySelectorAll('g'), //todo there should be :scope > g but IE 11 don`t support it )) { if ( getParentDeep( gElement, (element) => element.tagName.toUpperCase() === 'G', ) ) { continue; } let id: null | string = null; if (gElement.hasAttribute('data-playground-id')) { id = gElement.getAttribute('data-playground-id'); this.itemsIds.push(id!); } else { //do not worry // console.warn(`Element in playground do not have attribute "data-playground-id".`,gElement); } let gElementStatic: SVGGElement; let gElementDynamic: SVGGElement; const role = getParentDeepAttribute( gElement, 'data-playground-role', ); //todo all data-playground- to some config if (!role) { gElementStatic = gElement; gElementDynamic = gElement; } else if (role === 'static') { if (!id) { throw new Error( `You have to use data-playground-id when you are using data-playground-role.`, ); } gElementStatic = gElement; const dynamicElements = document.body.querySelectorAll( `*[data-playground-role="dynamic"] svg g[data-playground-id="${id}"]`, ); if (dynamicElements.length === 0) { throw new Error( `There is no dynamic part for data-playground-id="${id}".`, ); } else if (dynamicElements.length > 1) { throw new Error( `There are more (${dynamicElements.length}) dynamic parts for data-playground-id="${id}".`, ); } else { gElementDynamic = dynamicElements[0] as SVGGElement; } } else if (role === 'dynamic') { continue; } else { throw new Error(`Unknown data-playground-role="${role}".`); } //todo define all the data-playground- attributes and its meanig - eg. data-playground-role const gElementDynamicPlayground = new AttributeData< IPlaygroundData >(gElementDynamic, 'data-playground', defaultPlaygroundData()); //console.log('gElementDynamicPlayground',gElementDynamicPlayground); const playgroundActive = await forValueDefined( () => this.playgroundActive, ); playgroundActive.touchController.addInitialElement( gElementStatic, async (event) => { console.log(`Click on virgin element - making clone.`); const playgroundElementBoundingRect = this.playgroundElement.getBoundingClientRect(); const topLeftAnchor = new Vector2( playgroundElementBoundingRect.left - this.playgroundElement.scrollLeft, playgroundElementBoundingRect.top - this.playgroundElement.scrollTop, ); const svgElementBoundingRect = svgElement.getBoundingClientRect(); const topLeft = new Vector2( svgElementBoundingRect.left, svgElementBoundingRect.top, ) .subtractInPlace(topLeftAnchor) .subtractInPlace(moveBy) .subtractInPlace(this.playgroundSvgOffset); const gElementBoundingRect = getBoundingRectWithoutRotation( gElementStatic, ); const gElementNew = gElementDynamic.cloneNode( true, ) as SVGElement; const gElementNewPlayground = new AttributeData< IPlaygroundData >( gElementNew, 'data-playground', defaultPlaygroundData(), ); //------------------Now working with node clone let transform = svgTransformationDecode( gElementDynamic.getAttribute('transform') || undefined, //todo take new or old? Which is better??? ); transform = transform.add( Transformation.translate(topLeft), ); //todo use inPlace transform.rotateCenter = new Vector2( gElementBoundingRect.width / 2, gElementBoundingRect.height / 2, ); gElementNew.setAttribute( 'transform', svgTransformationEncode(transform), ); this.playgroundSvg.appendChild(gElementNew); gElementNewPlayground.data.amount = 1; gElementNewPlayground.save(); if (this.preserveOriginalSvg) { //return; } else if ( gElementDynamicPlayground.data.amount === -1 ) { //return; } else if (gElementDynamicPlayground.data.amount > 1) { gElementDynamicPlayground.data.amount -= 1; gElementDynamicPlayground.save(); //return; } else { svgElement.removeChild(gElementStatic); } playgroundActive.addSvgGroup( gElementNew as SVGGElement, ); return gElementNew; }, ); } } } private subscribersCall() { for (const subscriber of this.subscribers) { subscriber(); } } private async init() { await forImmediate(); const playgroundId = `playground-${uuidV4()}`; this.playgroundElement.classList.add(playgroundId); this.playgroundSvg = (document.createElement( 'svg', ) as any) as SVGElement; //todo better /* TODO: Do some cleanup on reinitialization const playgroungDeathElement = this.playgroundElement.parentElement!.querySelector( '.interactive-layer', ); if (playgroungDeathElement) { playgroungDeathElement.remove(); } */ const playgroungElement = document.createElement('div'); playgroungElement.classList.add('interactive-layer'); playgroungElement.appendChild(this.playgroundSvg); playgroungElement.innerHTML = playgroungElement.innerHTML; //This hack shows regenerated svg. Otherwise SVG is not visible. let debugLayerCanvas: HTMLCanvasElement | null = null; if (this.debugMode) { const debugLayerDiv = document.createElement('div'); debugLayerDiv.classList.add('interactive-layer'); debugLayerCanvas = document.createElement('canvas'); debugLayerCanvas.width = this.playgroundElement.getBoundingClientRect().width; debugLayerCanvas.height = this.playgroundElement.getBoundingClientRect().height; debugLayerDiv.appendChild(debugLayerCanvas); this.playgroundElement.insertBefore( debugLayerDiv, this.playgroundElement.children[0], ); } this.playgroundElement.insertBefore( playgroungElement, this.playgroundElement.children[0], ); this.playgroundSvg = playgroungElement.querySelector('svg')!; const boundingRect = this.playgroundSvg.getBoundingClientRect(); this.playgroundSvgOffset = new Vector2( -boundingRect.left, -boundingRect.top, ); this.playgroundSvg.style.left = `${this.playgroundSvgOffset.x}px`; this.playgroundSvg.style.top = `${this.playgroundSvgOffset.y}px`; const styleElement = createPlaygroundStyles(playgroundId); this.playgroundElement.appendChild(styleElement); const playgroundActive = new PlaygroundActive( this.playgroundSvg, this.playgroundElement, debugLayerCanvas ? { canvas: debugLayerCanvas, offset: this.playgroundSvgOffset } : null, () => getScroll(this.playgroundElement), ); playgroundActive.subscribe(() => { this.subscribersCall(); }); this.playgroundActive = playgroundActive; this.addListeners(); } }