satie
Version:
A sheet music renderer for the web
194 lines (159 loc) • 5.8 kB
text/typescript
/**
* This file is part of Satie music engraver <https://github.com/jnetterf/satie>.
* Copyright (C) Joshua Netterfield <joshua.ca> 2015 - present.
*
* Satie is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Satie is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Satie. If not, see <http://www.gnu.org/licenses/>.
*/
import {Component, ComponentLifecycle} from "react";
import * as invariant from "invariant";
import {isEqual, sortedIndex, indexOf} from "lodash";
import {ILayout} from "./document";
export interface IBaseProps {
layout: ILayout;
originX: number;
}
export interface IMetaComponent<P, S> extends Component<P, S>, ComponentLifecycle<P, S> {
context: {
originYByPartAndStaff: {[key: string]: number[]};
};
_record?: IRecord;
}
export interface ILookup {
x: number;
y: number;
}
export interface IRecord {
key: string;
obj: any;
x1: number;
x2: number;
y1: number;
y2: number;
originY: number;
}
/**
* A decorator that records the position of a component.
*/
export function Targetable<P extends IBaseProps, S>() {
return function decorator(component: {prototype: IMetaComponent<P, S>}) {
function updateMeta(self: IMetaComponent<P, S>, props: P) {
const layout = props.layout;
let originX: number = props.originX;
let originY = self.context.originYByPartAndStaff[
layout.part][layout.model.staffIdx || 1] || 0;
let newRecord: IRecord = {
key: props.layout.key,
obj: self,
x1: originX + props.layout.x - 2,
x2: originX + props.layout.x + props.layout.renderedWidth,
y1: originY - 60,
y2: originY + 60,
originY,
};
if (self._record) {
if (isEqual(newRecord, self._record)) {
return;
}
clearMeta(self);
}
if (isNaN(props.layout.renderedWidth)) {
console.warn("Missing rendered width in", props.layout.key);
return;
}
self._record = {
key: props.layout.key,
obj: self,
x1: originX + props.layout.x - 2,
x2: originX + props.layout.x + props.layout.renderedWidth,
y1: originY - 60,
y2: originY + 60,
originY,
};
set(self._record);
}
function clearMeta(self: IMetaComponent<P, S>) {
clear(self._record);
self._record = null;
}
// ---- //
let originalComponentWillMount =
component.prototype.componentWillMount;
component.prototype.componentWillMount =
function metaComponentWillMountWrapper() {
let self = this as IMetaComponent<P, S>;
updateMeta(self, self.props);
if (originalComponentWillMount) {
originalComponentWillMount.call(self);
}
};
// ---- //
let originalComponentWillUnmount =
component.prototype.componentWillUnmount;
component.prototype.componentWillUnmount =
function metaComponentWillUnmountWrapper() {
let self = this as IMetaComponent<P, S>;
clearMeta(self);
if (originalComponentWillUnmount) {
originalComponentWillUnmount.call(self);
}
};
// ---- //
let originalComponentWillReceiveProps =
component.prototype.componentWillReceiveProps;
component.prototype.componentWillReceiveProps =
function metaComponentWillReceiveProps(nextProps: P) {
let self = this as IMetaComponent<P, S>;
updateMeta(self, nextProps);
if (originalComponentWillReceiveProps) {
originalComponentWillReceiveProps.call(self);
}
};
};
}
let _sorted: IRecord[] = [];
let _weights: number[] = [];
function set(record: IRecord) {
let weight = weightForRecord(record);
let idx = sortedIndex(_weights, weight);
_sorted.splice(idx, 0, record);
_weights.splice(idx, 0, weight);
}
function clear(record: IRecord) {
let weight = weightForRecord(record);
let firstPossibleIdx = sortedIndex(_weights, weight);
let idx = indexOf(_sorted, record, firstPossibleIdx);
invariant(idx >= 0, `${record.key} not currently in array.`);
_sorted.splice(idx, 1);
_weights.splice(idx, 1);
}
export function get(lookup: ILookup): IRecord {
let {x, y} = lookup;
let weight = weightForLookup(lookup);
let firstPossibleIdx = sortedIndex(_weights, weight);
for (let i = firstPossibleIdx; i < _sorted.length; ++i) {
let record = _sorted[i];
if (_sorted[i].x1 <= x && _sorted[i].x2 >= x &&
_sorted[i].y1 <= y && _sorted[i].y2 >= y) {
return record;
}
}
return null;
}
function weightForRecord(record: IRecord) {
// In the future we should seperate by line.
return record.x2;
}
function weightForLookup(lookup: ILookup) {
return lookup.x;
}