您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
A small Blackprint Editor addons for PlayCanvas Editor
// ==UserScript== // @name Blackprint for PlayCanvas Editor // @namespace PlayCanvas Scripts // @match https://playcanvas.com/editor/scene/* // @match https://launch.playcanvas.com/* // @icon https://user-images.githubusercontent.com/11073373/141421213-5decd773-a870-4324-8324-e175e83b0f55.png // @grant none // @version 0.1.2 // @author StefansArya // @license MIT // @description A small Blackprint Editor addons for PlayCanvas Editor // ==/UserScript== /** External dependency loaded from this script * - Font-Awesome: UI icons * - ScarletsFrame: Frontend framework * - Blackprint Sketch: For nodes editor * - Blackprint Engine: For executing nodes * - Blackprint Skeleton: To import without executing module/nodes * - Timeplate: For cable animation (this deps can be removed) * - Eventpine: Simple event emitter * * Please scroll down to "Load external modules" for the version and URLs */ !(async ()=>{ let isGameMode = location.host === 'launch.playcanvas.com'; let frameworkURL = "https://cdn.jsdelivr.net/npm/[email protected]/dist/scarletsframe.dev.js"; await import(frameworkURL); let sf = window.sf; let { $ } = sf; window.$ = sf.$; // Required if we're going to use sf.Window sf.Window.frameworkPath = frameworkURL; // No state refresh // sf.hotReload?.(1); // ================ Load external modules ================ sf.loader.js([ "https://cdn.jsdelivr.net/npm/@blackprint/[email protected]/dist/engine.min.js", "https://cdn.jsdelivr.net/npm/@blackprint/[email protected]/dist/skeleton.min.js", "https://cdn.jsdelivr.net/npm/@blackprint/[email protected]/dist/blackprint.min.js", "https://cdn.jsdelivr.net/npm/@blackprint/[email protected]/dist/blackprint.sf.js", "https://cdn.jsdelivr.net/npm/[email protected]", "https://cdn.jsdelivr.net/npm/[email protected]", ], { ordered: true }); sf.loader.css([ "https://cdn.jsdelivr.net/npm/@blackprint/[email protected]/dist/blackprint.sf.css", 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.12.1/css/fontawesome.min.css', 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.12.1/css/solid.min.css', ]); let panelElement = $('#layout-assets .pcui-panel-content'); // ==================== On page ready ==================== $(function init(){ Blackprint.settings('visualizeFlow', true); // Below is for editor only if(isGameMode) return; editor.assets.on('add', refreshAssetIcon); editor.assets.on('move', refreshAssetIcon); editor.assets.on('load:all', refreshAssetIcon); panelElement.on('dblclick', '.type-blackprint', {capture: true}, async ev => { let el = ev.target.closest('.pcui-asset-grid-view-item'); let index = $.prevAll(el, 'div').length; let file = editor.assets.list()[index]?.get('file'); ev.stopImmediatePropagation(); ev.stopPropagation(); ev.preventDefault(); if(!file || !file.filename.endsWith('.bp.json')) throw new Error("Failed to get Blackprint file by element index"); showEditor(file); }) }); let _refreshAssetIcon; function refreshAssetIcon(){ clearTimeout(_refreshAssetIcon); _refreshAssetIcon = setTimeout(() => { let list = editor.assets.list(); for (let i=0; i < list.length; i++) { let asset = list[i]; if(asset.get('type') === 'json' && asset.get('name').endsWith('.bp')){ var els = panelElement.find('.pcui-gridview-item'); if(els.length === 0) continue; let el = els.eq(i); el.addClass('pcui-asset-grid-view-item-source') .addClass('type-blackprint'); el.find('span').removeClass('type-json'); el.find('img').attr('src', "https://user-images.githubusercontent.com/11073373/141421213-5decd773-a870-4324-8324-e175e83b0f55.png").css({ 'display': 'block', 'width': '58px', 'height': '58px', }); } } }, 500); } // ========= Initialize component's styles ========= let editors = {}; sf.$(document.head).append('<style id="blackprint-mini-css"></style>'); sf.$('style#blackprint-mini-css').html(` sf-m { display: block; } sf-space[blackprint] sf-m[name=container] { height: 100%; width: 100%; } @font-face { font-family: "Proxima Nova Regular"; src: url("https://playcanvas.com/static-assets/fonts/proxima_nova_regular.woff") format("woff"); font-weight: normal; font-style: normal; } @font-face { font-family: "Proxima Nova Light"; src: url("https://playcanvas.com/static-assets/fonts/proxima_nova_light.woff") format("woff"); font-weight: 200; font-style: normal; } @font-face { font-family: "Proxima Nova Bold"; src: url("https://playcanvas.com/static-assets/fonts/proxima_nova_bold.woff") format("woff"); font-weight: bold; font-style: normal; } @font-face { font-family: "Proxima Nova Thin"; src: url("https://playcanvas.com/static-assets/fonts/proxima_nova_thin_t.woff") format("woff"); font-weight: 100; font-style: normal; } bp-mini-editor { color: #b1b8ba; font-family: "Proxima Nova Regular", "Helvetica Neue", Arial, Helvetica, sans-serif; } bp-mini-editor > .container { background: #7c7c7cdb; position: fixed; width: 50vw; height: 50vh; z-index: 10; border-radius: 20px; overflow: hidden; border: 2px solid #5a5a5a; } bp-mini-editor > .container > .header{ background: #181818; width: 100%; height: 20px; text-align: center; cursor: default; z-index: 2; position: relative; font-weight: bold; } bp-mini-editor > .container > sf-space[blackprint]{ height: calc(100% - 20px); } bp-mini-editor > .container > .close, bp-mini-editor > .container > .copy-clipboard { position: absolute; font-weight: bold; cursor: pointer; padding: 0 10px; z-index: 2; } bp-mini-editor > .container > .close { top: 0; right: 20px; } bp-mini-editor > .container > .new-window { position: absolute; right: 70px; top: 2px; z-index: 2; font-size: 14px; cursor: pointer; } bp-mini-editor > .container > .copy-clipboard { top: 0; left: 20px; } bp-mini-editor a { color: #bfc0c0; } bp-mini-editor a span { color: black; } bp-instance-list .container > .list-closed { position: fixed; width: 30px; height: 30px; background: #00000099; color: white; letter-spacing: 2px; border-radius: 20px; padding: 6px; padding-left: 20px; top: 50%; left: -15px; font-family: sans-serif; box-shadow: 0 0 4px #c1c1c1; cursor: pointer; transform: translateY(-50%); } bp-instance-list .container > .list-closed > img { width: 100%; height: 100%; } bp-instance-list .container.opened > .list-closed { display: none; } bp-instance-list .container > .list-opened { top: 50%; border-radius: 0 10px 10px 0; box-shadow: 0 0 10px black; color: white; left: 0; transform: translate(-100%, -50%); background: #383838fa; position: fixed; opacity: 0; transition: 0.7s ease-in-out; transition-property: opacity, transform; font-size: 16px; width: 200px; padding: 5px 10px; } bp-instance-list .container.opened > .list-opened { opacity: 1; transform: translate(0%, -50%); } bp-instance-list .container > .list-opened > .close { float: right; cursor: pointer; } bp-instance-list .container > .list-opened > .list-title { text-align: center; } bp-instance-list .container > .list-opened > .instance-list { margin-top: 5px; padding-top: 5px; border-top: 1px dashed white; } bp-instance-list .container > .list-opened > .instance-list { cursor: pointer; } `); // ========= Initialize bp-mini-editor component ========= sf.component.html('bp-mini-editor', ` <div class="container" style="left:{{ x }}px; top: {{ y }}px;"> <div class="header" @dragmove="moveEditor(event)" title="{{ !isGameMode ? 'This editor is used only for previewing node connection without loading or executing any nodes' : 'This editor can be be used to modify node data flow in realtime' }}"> {{ title }} - Blackprint {{ isGameMode ? 'Nodes' : 'Skeleton'}} Editor </div> <div class="copy-clipboard" style="display: {{ isGameMode ? 'show' : '' }}" title="Save to clipboard" @click="save()">copy</div> <div class="close" title="Close" @click="close()">x</div> <div class="new-window" title="Open in new window" @click="cloneContainer()"> <i class="fa fa-window-restore"></i> </div> </div> `); sf.component('bp-mini-editor', class extends sf.Model { constructor(){ super(); this.isGameMode = isGameMode; this.x = 310; this.y = 50; this.title = "Loading..."; } init(){ if(this.sketchEl || !this.instance) return; this.sketchEl = this.instance.cloneContainer(); this.$el('.container').append(this.sketchEl); registerEditorListener($('sf-m[name="container"]', this.sketchEl)[0].model); // Delay a bit until the container is attached to DOM and initialized return new Promise(r => setTimeout(r, 500)); } async import(title, json){ this.instance ??= new Blackprint.Sketch(); await this.init(); this.title = title; // Load node skeleton only if in PlayCanvas editor // Load and use engine instance if in game mode await this.instance.importJSON(json, {isSkeletonInstance: !isGameMode}); await this.onLoaded(); } async onLoaded(){ let sketch = this.instance; await sketch.recalculatePosition(); let Ref = sketch.scope('container'); let {offsetHeight, offsetWidth} = Ref.$el[0].parentElement; let nodes = Ref.nodeScope.list; let maxX = 0, maxY = 0; let W = 0, H = 0; for (var i = 0; i < nodes.length; i++) { let node = nodes[i]; if(maxX < node.x){ maxX = node.x; W = node.w || 0; } if(maxY < node.y){ maxY = node.y; H = node.h || 0; } } maxX += W + 50; maxY += H + 50; let A = offsetWidth / maxX; let B = offsetHeight / maxY; let decidedScale = A < B ? A : B; decidedScale = decidedScale - (decidedScale % 0.01); if(decidedScale === 0) { console.log("Unexpected: scaling the container to zero"); decidedScale = 1; } Ref.scale = decidedScale > 1 ? 1 : decidedScale; Ref.size.w = offsetWidth / decidedScale; Ref.size.h = offsetHeight / decidedScale; } save(){ navigator.clipboard.writeText(this.instance.exportJSON()); } moveEditor(ev){ this.x += ev.movementX; this.y += ev.movementY; if(ev.type === 'pointerup' || ev.type === 'touchend' || ev.type === 'mouseup') this.instance?.recalculatePosition(); } cloneContainer(){ let sketch = this.instance; let title = sketch._funcMain != null ? `"${sketch._funcMain.title}" Function` : `Main`; // Clone into new window new sf.Window({ title: `Sketch: ${title}`, templateHTML: sketch.cloneContainer() // Clone 2 }); } close(){ this.$el.remove(); } destroy(){ delete editors[this.hash]; } }); async function showEditor(file){ if(!editors[file.hash]){ let component = $('<bp-mini-editor></bp-mini-editor>')[0]; editors[file.hash] = component; $(document.body).append(component); } let editor = editors[file.hash]; let data = await $.getJSON(file.url); editor.model.import(file.filename, data); editor.model.hash = file.hash; } function registerEditorListener(My){ function pointerOver(targetEl){ if(targetEl.tagName === 'path'){ targetEl.parentElement.model.disconnect(); } } function pmEvent(ev){ if(!ev.altKey) return temp.off('pointermove', pmEvent); pointerOver(ev.target); } function tmEvent(ev){ if(!ev.altKey) return temp.off('touchmove', tmEvent); let touch = ev.touches[0]; pointerOver(document.elementFromPoint(touch.clientX, touch.clientY)); } let temp = My.$el('sf-m[name="cables"]'); let evRegistered = false; temp.on('pointerdown', function(ev){ if(ev.altKey && !evRegistered){ evRegistered = true; if(ev.pointerType === 'touch') temp.on('touchmove', tmEvent); else temp.on('pointermove', pmEvent); } }) .on('pointerup', function(ev){ if(evRegistered){ evRegistered = false; if(ev.pointerType === 'touch') temp.off('touchmove', tmEvent); else temp.off('pointermove', pmEvent); } }); } // ======== Initialize bp-instance-list component ======== sf.component.html('bp-instance-list', ` <div class="container {{ listOpened ? 'opened' : ''}}" style="display: {{ isGameMode ? '' : 'none' }}"> <div class="list-closed" @click="listOpened = true"> <img src="https://user-images.githubusercontent.com/11073373/141421213-5decd773-a870-4324-8324-e175e83b0f55.png"> </div> <div class="list-opened"> <div class="close" @click="listOpened = false">x</div> <div class="list-title">Instance List <span>🌿</span></div> <div class="instance-list"> <div sf-each="i, val in instances" @click="open(val)"> {{ i+1 }}. {{ val._pcAssetName }} </div> </div> <div class="list-title" style="display: {{ ready ? 'none' : '' }}">Loading instances</div> </div> </div> `); sf.component('bp-instance-list', class extends sf.Model { constructor(){ super(); this.isGameMode = isGameMode; this.listOpened = false; this.ready = false; this.instances = window.blackprintList ??= []; window.onBlackprintReady = () => { this.ready = true; } } async open(instance){ if(!editors[instance._pcAssetName]){ let component = $('<bp-mini-editor></bp-mini-editor>')[0]; $(document.body).append(component); editors[instance._pcAssetName] = component; } let editor = editors[instance._pcAssetName]; let model = editor.model; model.instance = instance; model.hash = model.title = instance._pcAssetName; await model.init(); model.onLoaded(); } }); $(function(){ let component = $('<bp-instance-list></bp-instance-list>')[0]; $(document.body).append(component); let lastInteractEl = null; function checkIfHasSelection(skipSelectionCheck){ // Skip textbox/input element let tagName = lastInteractEl.tagName; if(tagName === 'INPUT' || tagName === 'TEXTAREA') return; let sketch = lastInteractEl.closest('sf-m[name="container"]')?.model.$space.sketch; if(sketch == null) return; let container = sketch.scope('container'); // Skip if no selected nodes/cables if(!skipSelectionCheck && container.nodeScope.selected.length === 0 && container.cableScope.selected.length === 0){ return; } return {sketch, container}; } $(window) .on('pointerdown', function(ev){ lastInteractEl = ev.target; }) .on('keydown', async (ev) => { if(ev.key !== 'Delete') return; let passed = checkIfHasSelection(); if(!passed) return; let {sketch, container} = passed; if(container.nodeScope.selected.length === 0 && container.cableScope.selected.length === 0) return; if(ev.key === 'Delete'){ let selected = container.nodeScope.selected; for(let i=0; i < selected.length; i++) sketch.deleteNode(selected[i]); selected.splice(0); selected = container.cableScope.selected; for(let i=0; i < selected.length; i++) selected[i].disconnect(); selected.splice(0); } }) }); })();