blockbench/js/preview/reference_images.js

848 lines
25 KiB
JavaScript

class ReferenceImage {
constructor(data = {}) {
this.name = '';
this.layer = '';
this.scope = '';
this.position = [0, 0];
this.size = [0, 0];
this.flip_x = false;
this.flip_y = false;
this.rotation = 0;
this.opacity = 0;
this.visibility = true;
this.clear_mode = false;
this.attached_side = 0;
this.source = '';
this.uuid = guid();
this.condition = data.condition;
for (let key in ReferenceImage.properties) {
ReferenceImage.properties[key].reset(this);
}
this.position.V2_set(window.innerWidth/2, window.innerHeight/2);
this.node = Interface.createElement('div', {class: 'reference_image'});
addEventListeners(this.node, 'mousedown touchstart', event => this.select());
this.node.addEventListener('contextmenu', event => {
this.openContextMenu(event);
})
this.img = new Image();
this.img.style.display = 'none';
this.node.append(this.img);
this._modify_nodes = [];
this.defaults = data;
this.dark_background = false;
this.image_is_loaded = false;
this.auto_aspect_ratio = true;
this.img.onload = () => {
let was_image_loaded = this.image_is_loaded;
this.image_is_loaded = true;
if (this.auto_aspect_ratio) {
let original_size = this.size[1];
this.size[1] = this.size[0] / this.aspect_ratio;
if (original_size != this.size[1]) this.update();
} else if (!was_image_loaded) {
this.update();
}
this.updateClearMode();
}
this.img.onerror = () => {
this.image_is_loaded = false;
}
this.extend(data);
}
get aspect_ratio() {
if (this.img && this.img.naturalWidth && this.img.naturalHeight) {
return this.img.naturalWidth / this.img.naturalHeight;
} else {
return 1;
}
}
extend(data) {
if (data.size instanceof Array) this.auto_aspect_ratio = false;
for (let key in ReferenceImage.properties) {
ReferenceImage.properties[key].merge(this, data)
}
}
getSaveCopy() {
/*let dataUrl;
if (isApp && this.image && this.image.substr(0, 5) != 'data:') {
let canvas = document.createElement('canvas');
canvas.width = this.imgtag.naturalWidth;
canvas.height = this.imgtag.naturalHeight;
let ctx = canvas.getContext('2d');
ctx.drawImage(this.imgtag, 0, 0);
dataUrl = canvas.toDataURL('image/png');
}*/
let copy = {};
for (let key in ReferenceImage.properties) {
if (this[key] != ReferenceImage.properties[key].default) ReferenceImage.properties[key].copy(this, copy);
}
return copy;
}
resolveCondition() {
if (!Condition(this.condition)) return false;
if (this.layer == 'blueprint') {
return Preview.all.find(p => p.isOrtho && p.angle == this.attached_side) !== undefined;
}
return true;
}
addAsReference(save) {
Project.reference_images.push(this);
this.scope = 'project';
this.update();
if (save) this.save();
return this;
}
addAsGlobalReference(save) {
ReferenceImage.global.push(this);
this.scope = 'global';
this.update();
if (save) this.save();
return this;
}
addAsBuiltIn(save) {
ReferenceImage.built_in.push(this);
this.scope = 'built_in';
this.update();
if (save) this.save();
return this;
}
select(force) {
if (!force && this.selected) return this;
if (this.scope == 'built_in') return this;
if (ReferenceImage.selected && ReferenceImage.selected != this) {
ReferenceImage.selected.unselect();
}
if (!ReferenceImageMode.active) {
ReferenceImageMode.activate()
}
ReferenceImage.selected = this;
this.update();
return this;
}
unselect() {
if (ReferenceImage.selected == this) ReferenceImage.selected = null;
this.update();
return this;
}
get selected() {
return ReferenceImage.selected == this;
}
save() {
this.position[0] = Math.round(this.position[0]);
this.position[1] = Math.round(this.position[1]);
this.size[0] = Math.round(this.size[0]);
this.size[1] = Math.round(this.size[1]);
this.rotation = Math.roundTo(this.rotation, 2);
switch (this.scope) {
case 'project':
Project.saved = false;
break;
case 'global':
ReferenceImageMode.saveGlobalReferences();
break;
case 'built_in':
// todo: save
break;
}
return this;
}
update() {
if (!Interface.preview) return this;
let shown = this.resolveCondition();
if (!shown) {
this.node.remove();
return this;
}
this.node.setAttribute('reference_layer', this.layer);
switch (this.layer) {
case 'background': {
Interface.preview.querySelector('.clamped_reference_images').append(this.node);
break;
}
case 'viewport': {
Interface.preview.querySelector('.clamped_reference_images').append(this.node);
break;
}
case 'blueprint': {
Interface.preview.querySelector('.clamped_reference_images').append(this.node);
break;
}
case 'float': default: {
Interface.work_screen.append(this.node);
break;
}
}
this.updateTransform();
this.img.style.display = (this.visibility && this.image_is_loaded) ? 'block' : 'none';
this.img.style.opacity = this.opacity;
let transforms = [];
if (this.rotation) transforms.push(`rotate(${this.rotation}deg)`);
if (this.flip_x) transforms.push('scaleX(-1)');
if (this.flip_y) transforms.push('scaleY(-1)');
this.img.style.transform = transforms.join(' ');
if (this.img.src != this.source) this.img.src = this.source;
this.img.style.imageRendering = (this.img.naturalWidth > this.size[0]) ? 'auto' : 'pixelated';
if (!this.selected && this.clear_mode) {
let light_mode = document.body.classList.contains('light_mode');
this.node.style.filter = (light_mode != this.dark_background ? '' : 'invert(1) ') + 'contrast(1.2)';
this.node.style.mixBlendMode = 'lighten';
} else {
this.node.style.filter = '';
this.node.style.mixBlendMode = '';
}
// Select
if (this.selected && !this._modify_nodes.length) {
this.setupEditHandles()
}
// Unselect
if (!this.selected && this._modify_nodes.length) {
this.node.classList.remove('selected');
this._modify_nodes.forEach(node => node.remove());
this._modify_nodes.empty();
}
if (this.selected) {
this.node.querySelector('div.reference_image_toolbar .tool[tool_id=flip_x]').classList.toggle('enabled', this.flip_x);
this.node.querySelector('div.reference_image_toolbar .tool[tool_id=flip_y]').classList.toggle('enabled', this.flip_y);
this.node.querySelector('div.reference_image_toolbar .tool[tool_id=visibility]').classList.toggle('enabled', this.visibility);
}
return this;
}
getZoomLevel() {
let preview = this.layer == 'blueprint' && Preview.all.find(p => p.isOrtho && p.angle == this.attached_side);
return preview ? preview.camOrtho.zoom * 2 : 1;
}
updateTransform() {
if (!this.node.isConnected) return this;
let preview = this.layer == 'blueprint' && Preview.all.find(p => p.isOrtho && p.angle == this.attached_side);
if (preview) {
let zoom = this.getZoomLevel();;
let pos_x = this.position[0];
let pos_y = this.position[1];
pos_x = preview.controls.target[preview.camOrtho.backgroundHandle[0].a] * zoom * 20;
pos_y = preview.controls.target[preview.camOrtho.backgroundHandle[1].a] * zoom * 20;
pos_x *= preview.camOrtho.backgroundHandle[0].n === true ? 1 : -1;
pos_y *= preview.camOrtho.backgroundHandle[1].n === true ? 1 : -1;
pos_x += preview.width/2;
pos_y += preview.height/2;
pos_x += (this.position[0] * zoom) - (this.size[0] * zoom) / 2;
pos_y += (this.position[1] * zoom) - (this.size[1] * zoom) / 2;
this.node.style.width = (this.size[0] * zoom) + 'px';
this.node.style.height = (this.size[1] * zoom) + 'px';
this.node.style.left = pos_x + 'px';
this.node.style.top = pos_y + 'px';
} else {
this.node.style.width = this.size[0] + 'px';
this.node.style.height = this.size[1] + 'px';
this.node.style.left = (Math.clamp(this.position[0], 0, this.node.parentNode.clientWidth) - this.size[0]/2) + 'px';
this.node.style.top = (Math.clamp(this.position[1], 0, this.node.parentNode.clientHeight) - this.size[1]/2) + 'px';
}
return this;
}
updateClearMode() {
if (this.clear_mode && this.image_is_loaded) {
let average_color = getAverageRGB(this.img);
this.dark_background = (average_color.r + average_color.g + average_color.b) < 380;
} else {
this.dark_background = false;
}
}
detach() {
this.node.remove();
}
setupEditHandles() {
let self = this;
this.node.classList.add('selected');
let resize_corners = ['nw', 'ne', 'sw', 'se'].map(direction => {
let corner = Interface.createElement('div', {class: 'reference_image_resize_corner '+direction});
let sign_x = direction[1] == 'e' ? 1 : -1;
let sign_y = direction[0] == 's' ? 1 : -1;
addEventListeners(corner, 'mousedown touchstart', e1 => {
convertTouchEvent(e1);
let original_position = this.position.slice();
let original_size = this.size.slice();
let move = (e2) => {
convertTouchEvent(e2);
let offset = [
(e2.clientX - e1.clientX),
(e2.clientY - e1.clientY),
];
this.size[0] = Math.max(original_size[0] + offset[0] * sign_x, 48);
this.position[0] = original_position[0] + offset[0] / 2, 0;
if (!e2.ctrlOrCmd && !Pressing.overrides.ctrl) {
offset[1] = sign_y * (this.size[0] / this.aspect_ratio - original_size[1]);
}
this.size[1] = Math.max(original_size[1] + offset[1] * sign_y, 32);
this.position[1] = original_position[1] + offset[1] / 2, 0;
if (this.layer !== 'blueprint') {
this.position[0] = Math.clamp(this.position[0], 0, this.node.parentNode.clientWidth);
this.position[1] = Math.clamp(this.position[1], 0, this.node.parentNode.clientHeight);
}
this.update();
}
let stop = (e2) => {
convertTouchEvent(e2);
removeEventListeners(document, 'mousemove touchmove', move);
removeEventListeners(document, 'mouseup touchend', stop);
this.save();
}
addEventListeners(document, 'mousemove touchmove', move);
addEventListeners(document, 'mouseup touchend', stop);
})
this.node.append(corner);
return corner;
});
this._modify_nodes.push(...resize_corners);
let rotate_handle = Interface.createElement('div', {class: 'reference_image_rotate_handle'}, Blockbench.getIconNode('rotate_right'));
addEventListeners(rotate_handle, 'mousedown touchstart', e1 => {
convertTouchEvent(e1);
let original_rotation = this.rotation;
let offset = $(this.node).offset();
let center = [
offset.left + this.size[0]/2,
offset.top + this.size[1]/2,
]
let initial_angle = null;
let move = (e2) => {
convertTouchEvent(e2);
let angle = Math.radToDeg(Math.atan2(
e2.clientY - center[1],
e2.clientX - center[0],
))
if (initial_angle === null) initial_angle = angle;
if (angle != initial_angle) {
let target_rotation = Math.trimDeg(original_rotation + angle - initial_angle);
this.rotation = Math.snapToValues(target_rotation, [-180, -90, 0, 90, 180], 3);
this.update();
}
}
let stop = (e2) => {
convertTouchEvent(e2);
removeEventListeners(document, 'mousemove touchmove', move);
removeEventListeners(document, 'mouseup touchend', stop);
this.save();
}
addEventListeners(document, 'mousemove touchmove', move);
addEventListeners(document, 'mouseup touchend', stop);
})
this.node.append(rotate_handle);
this._modify_nodes.push(rotate_handle);
this.toolbar = Interface.createElement('div', {class: 'reference_image_toolbar'});
this.node.append(this.toolbar);
this._modify_nodes.push(this.toolbar);
// Controls
function addButton(id, icon, click) {
let node = Interface.createElement('div', {class: 'tool', tool_id: id}, Blockbench.getIconNode(icon));
self.toolbar.append(node);
node.onclick = click;
}
addButton('layer', 'flip_to_front', (event) => {
let layers = {
background: 'reference_image.layer.background',
viewport: 'reference_image.layer.viewport',
float: 'reference_image.layer.float',
}
if (Preview.selected.angle) {
layers.blueprint = 'reference_image.layer.blueprint';
}
let options = Object.keys(layers).map(key => {
return {
name: layers[key],
icon: this.layer == key ? 'radio_button_checked' : 'radio_button_unchecked',
click: () => {
this.changeLayer(key);
this.update().save();
}
}
})
new Menu(options).open(event.target);
});
addButton('flip_x', 'icon-mirror_x', () => {
self.flip_x = !self.flip_x;
self.update().save();
});
addButton('flip_y', 'icon-mirror_y', () => {
self.flip_y = !self.flip_y;
self.update().save();
});
this.opacity_slider = new NumSlider({
id: 'slider_reference_image_opacity',
private: true,
condition: () => true,
get() {
return self.opacity * 100;
},
change(modify) {
self.opacity = Math.clamp(modify(self.opacity*100) / 100, 0, 1);
self.update();
},
onAfter() {
self.save();
},
sensitivity: 5,
settings: {
min: 0, max: 100, step: 1, show_bar: true
}
}).toElement(this.toolbar).update();
addButton('visibility', 'visibility', () => {
self.visibility = !self.visibility;
self.update().save();
});
if (!this._edit_events_initialized) {
addEventListeners(this.node, 'mousedown touchstart', e1 => {
if (!e1.target.classList.contains('reference_image')) return;
convertTouchEvent(e1);
let original_position = this.position.slice();
let zoom = this.getZoomLevel();
let move = (e2) => {
convertTouchEvent(e2);
let offset = [
(e2.clientX - e1.clientX),
(e2.clientY - e1.clientY),
];
this.position[0] = original_position[0] + offset[0] / zoom;
this.position[1] = original_position[1] + offset[1] / zoom;
if (this.layer !== 'blueprint') {
this.position[0] = Math.clamp(this.position[0], 0, this.node.parentNode.clientWidth);
this.position[1] = Math.clamp(this.position[1], 0, this.node.parentNode.clientHeight);
}
this.update();
}
let stop = (e2) => {
convertTouchEvent(e2);
removeEventListeners(document, 'mousemove touchmove', move);
removeEventListeners(document, 'mouseup touchend', stop);
this.save();
}
addEventListeners(document, 'mousemove touchmove', move);
addEventListeners(document, 'mouseup touchend', stop);
})
this.node.addEventListener('dblclick', event => {
this.propertiesDialog();
})
this._edit_events_initialized = true;
}
return this;
}
projectMouseCursor(x, y) {
if (!this.resolveCondition() || !this.visibility) return false;
let rect = this.img.getBoundingClientRect();
if (x > rect.x && y > rect.y && x < rect.right && y < rect.bottom) {
let lerp_x = Math.getLerp(rect.x, rect.right, x);
let lerp_y = Math.getLerp(rect.y, rect.bottom, y);
if (this.flip_x) lerp_x = 1 - lerp_x;
if (this.flip_y) lerp_y = 1 - lerp_y;
return [
Math.floor(Math.min(lerp_x, 0.9999) * this.img.naturalWidth),
Math.floor(Math.min(lerp_y, 0.9999) * this.img.naturalHeight),
]
}
return false;
}
openContextMenu(event) {
this.menu.open(event, this);
return this;
}
reset() {
return this;
}
async delete(force) {
if (!force) {
let result = await new Promise(resolve => Blockbench.showMessageBox({
title: 'data.reference_image',
message: 'message.delete_reference_image',
buttons: ['dialog.confirm', 'dialog.cancel']
}, resolve));
if (result == 1) return;
}
if (ReferenceImage.selected == this) ReferenceImage.selected = null;
this.update();
switch (this.scope) {
case 'project': Project.reference_images.remove(this); break;
case 'global': ReferenceImage.global.remove(this); break;
case 'built_in': ReferenceImage.built_in.remove(this); break;
}
this.save();
this.node.remove();
}
changeLayer(layer) {
if (layer == this.layer) return;
if (layer == 'blueprint' && Preview.selected?.angle) {
this.attached_side = Preview.selected.angle;
this.position.V2_set(0, 0);
}
this.layer = layer;
return this;
}
changeScope(new_scope) {
if (new_scope == this.scope) return this;
switch (this.scope) {
case 'project': Project.reference_images.remove(this); break;
case 'global': ReferenceImage.global.remove(this); break;
//case 'built_in': ReferenceImage.built_in.remove(this); break;
}
this.scope = new_scope;
switch (this.scope) {
case 'project': Project.reference_images.push(this); break;
case 'global': ReferenceImage.global.push(this); break;
//case 'built_in': ReferenceImage.built_in.push(this); break;
}
return this;
}
propertiesDialog() {
new Dialog('reference_image_properties', {
title: 'data.reference_image',
form: {
source: {type: 'file', label: 'reference_image.image', condition: () => isApp && PathModule.isAbsolute(this.source), value: this.source, extensions: ['png', 'jpg', 'jpeg']},
layer: {type: 'select', label: 'reference_image.layer', value: this.layer, options: {
background: 'reference_image.layer.background',
viewport: 'reference_image.layer.viewport',
float: 'reference_image.layer.float',
blueprint: Preview.selected.angle ? 'reference_image.layer.blueprint' : undefined,
}},
scope: {type: 'select', label: 'reference_image.scope', value: this.scope, options: {
project: 'reference_image.scope.project',
global: 'reference_image.scope.global',
}},
position: {type: 'vector', label: 'reference_image.position', dimensions: 2, value: this.position},
size: {type: 'vector', label: 'reference_image.size', dimensions: 2, value: this.size},
rotation: {type: 'number', label: 'reference_image.rotation', value: this.rotation},
opacity: {type: 'range', label: 'reference_image.opacity', editable_range_label: true, value: this.opacity, min: 0, max: 1}
},
onConfirm: (result) => {
let clear_mode_before = this.clear_mode;
this.changeLayer(result.layer);
this.changeScope(result.scope);
this.extend(result);
if (this.clear_mode != clear_mode_before) {
this.updateClearMode();
}
this.update().save();
}
}).show();
return this;
}
}
ReferenceImage.prototype.menu = new Menu([
/**
* Todo
Visibility
Restore
Refresh file
*/
{
id: 'clear_mode',
name: 'reference_image.clear_mode',
icon: (ref) => ref.clear_mode,
condition: ref => ref.layer == 'blueprint',
click(ref) {
ref.clear_mode = !ref.clear_mode;
ref.updateClearMode();
ref.update().save();
// Preview
if (ref.clear_mode) {
ref.unselect();
setTimeout(() => ref.select(), 400);
}
}
},
{
name: 'reference_image.layer',
icon: 'list',
children: (reference) => {
let layers = {
background: 'reference_image.layer.background',
viewport: 'reference_image.layer.viewport',
float: 'reference_image.layer.float',
}
if (Preview.selected.angle) {
layers.blueprint = 'reference_image.layer.blueprint';
}
let children = [];
for (let key in layers) {
children.push({
id: key,
name: layers[key],
icon: reference.layer == key ? 'radio_button_checked' : 'radio_button_unchecked',
click() {
reference.changeLayer(key);
reference.update().save();
}
})
}
return children;
}
},
'_',
'delete',
'_',
{
name: 'menu.texture.properties',
icon: 'list',
click(instance, b) {
instance.propertiesDialog();
}
}
])
new Property(ReferenceImage, 'string', 'name', {default: 'Reference'});
new Property(ReferenceImage, 'string', 'layer', {default: 'float'}); // reference, blueprint
new Property(ReferenceImage, 'string', 'scope', {default: 'global'}); // reference, blueprint
new Property(ReferenceImage, 'vector2', 'position');
new Property(ReferenceImage, 'vector2', 'size', {default: [400, 300]});
new Property(ReferenceImage, 'boolean', 'flip_x');
new Property(ReferenceImage, 'boolean', 'flip_y');
new Property(ReferenceImage, 'number', 'rotation');
new Property(ReferenceImage, 'number', 'opacity', {default: 1});
new Property(ReferenceImage, 'boolean', 'visibility', {default: true});
new Property(ReferenceImage, 'boolean', 'clear_mode');
new Property(ReferenceImage, 'string', 'attached_side', {default: 'north'});
new Property(ReferenceImage, 'string', 'source');
ReferenceImage.selected = null;
ReferenceImage.built_in = [];
ReferenceImage.global = [];
Object.defineProperty(ReferenceImage, 'current_project', {
get() {
return Project.reference_images || [];
}
})
Object.defineProperty(ReferenceImage, 'all', {
get() {
return ReferenceImage.built_in.concat(ReferenceImage.global, ReferenceImage.current_project);
}
})
Object.defineProperty(ReferenceImage, 'active', {
get() {
return ReferenceImage.all.filter(ref => ref.resolveCondition());
}
})
ReferenceImage.updateAll = function() {
ReferenceImage.all.forEach(ref => {
ref.update();
})
}
StateMemory.init('global_reference_images', 'array');
StateMemory.global_reference_images.forEach(template => {
new ReferenceImage(template).addAsGlobalReference();
});
const ReferenceImageMode = {
active: false,
toolbar: null,
activate() {
ReferenceImageMode.active = true;
Interface.work_screen.classList.add('reference_image_mode');
Interface.work_screen.append(ReferenceImageMode.toolbar.node);
ReferenceImage.updateAll();
setTimeout(_ => {
if (!ReferenceImage.selected && ReferenceImage.active[0]) {
ReferenceImage.active[0].select();
}
}, 1);
},
deactivate() {
ReferenceImageMode.active = false;
if (ReferenceImage.selected) ReferenceImage.selected.unselect();
Interface.work_screen.classList.remove('reference_image_mode');
ReferenceImageMode.toolbar.node.remove();
ReferenceImage.updateAll();
},
async importReferences(files) {
let save_mode = await new Promise(resolve => {
let icon = new Image();
icon.src = files[0].content;
Blockbench.showMessageBox({
title: 'action.add_reference_image',
message: 'Select where to load the reference image',
icon,
commands: {
project: 'Save in project',
app: 'Save in Blockbench',
}
}, resolve)
})
files.forEach(file => {
let ref = new ReferenceImage({source: file.content, name: file.name});
if (save_mode == 'project') {
ref.addAsReference(true);
} else {
ref.addAsGlobalReference(true);
}
ref.select();
})
},
saveGlobalReferences() {
StateMemory.global_reference_images = ReferenceImage.global.map(ref => ref.getSaveCopy());
StateMemory.save('global_reference_images');
}
}
BARS.defineActions(function() {
new Action('edit_reference_images', {
icon: 'wallpaper',
category: 'view',
click() {
if (ReferenceImageMode.active) {
ReferenceImageMode.deactivate()
} else {
ReferenceImageMode.activate()
}
}
});
new Action('add_reference_image', {
icon: 'add_photo_alternate',
category: 'view',
click() {
if (!ReferenceImageMode.active) {
ReferenceImageMode.activate()
}
Blockbench.import({
resource_id: 'reference_image',
extensions: ['png', 'jpg', 'jpeg', 'bmp', 'tiff', 'tif', 'gif'],
type: 'Image',
readtype: 'image'
}, async function(files) {
ReferenceImageMode.importReferences(files);
}, 'image', false)
}
});
new Action('reference_image_from_clipboard', {
icon: 'fa-clipboard',
category: 'view',
click() {
if (!ReferenceImageMode.active) {
ReferenceImageMode.activate()
}
if (isApp) {
var image = clipboard.readImage().toDataURL();
if (image.length > 32) {
ReferenceImageMode.importReferences([{content: image, name: 'Pasted'}]);
}
} else {
navigator.clipboard.read().then(content => {
if (content && content[0] && content[0].types.includes('image/png')) {
content[0].getType('image/png').then(blob => {
let url = URL.createObjectURL(blob);
if (image.length > 32) {
ReferenceImageMode.importReferences([{content: url, name: 'Pasted'}]);
}
})
}
})
}
}
});
new Action('reference_image_list', {
icon: 'list',
category: 'view',
click(e) {
new Menu('apply_display_preset', this.children(), {searchable: false}).open(e.target, 'wrong context');
},
children() {
let list = [];
function getSubMenu(reference) {
return [
/** Todo: add options
* Center
* Delete
* Visibility
*/
{
name: 'Properties...',
icon: 'list',
click() {
reference.propertiesDialog();
}
}
]
}
ReferenceImage.current_project.forEach(reference => {
list.push({
name: (reference.name || 'Unknown').substring(0, 24), id: reference.uuid,
icon: 'icon-blockbench_file',
children: getSubMenu(reference)
});
});
list.push('_');
ReferenceImage.global.forEach(reference => {
list.push({
name: (reference.name || 'Unknown').substring(0, 24), id: reference.uuid,
icon: 'icon-blockbench_inverted',
children: getSubMenu(reference)
});
});
list.push('_');
ReferenceImage.built_in.forEach(reference => {
if (!Condition(reference.condition)) return;
list.push({
name: (reference.name || 'Unknown').substring(0, 24), id: reference.uuid,
icon: 'settings'
});
});
return list;
}
});
})
Interface.definePanels(function() {
ReferenceImageMode.toolbar = new Toolbar('reference_images', {
children: [
'add_reference_image',
'reference_image_from_clipboard',
'reference_image_list',
]
})
})