-- Version 0.01 assert((nwc.VERSIONDATE or '00000000') >= '20161207', 'This plugin requires version 2.75a') --[[----------------------------------------------------------------------------------------- CueStaff.test This object can be used to add special lines that span across a selection of notes. An optional text instruction can accompany the line. @Pen This sets the pen style for the line. @PenW This sets the thickness for the line. @DashLeft ___ @DashBars ___ @DashRight ___ --]]----------------------------------------------------------------------------------------- local userObjTypeName = ... local _spec = { {id='Scale', label='Sc&ale', type='float', default=0.75, min=0.5, max=3, step=0.1}, {id='Overhang', label='O&verhang', type='float', default=1, min=-100, max=100, step=0.5}, {id='DashLeft', label='Connect on &left', type='bool', default=false }, {id='DashBars', label='Connect &bars', type='bool', default=true }, {id='DashRight', label='Connect on &right', type='bool', default=false }, {id='Pen', label='&Line Type', type='enum', default=nwc.txt.DrawPenStyle[1], list=nwc.txt.DrawPenStyle}, {id='PenWidth', label='T&hickness', type='float', default=1, min=0.1, max=4, step=0.1}, } local function _create(t) ---- t.Class = 'Standard' end local function _spin(t, dir) t.Span = t.Span + dir _audit(t) end local function mirrorStaffLines(scale, x1, x2, lines) local y = -scale * (lines - 1) for i = 1, lines do nwcdraw.line(x1, y, x2, y) y = y + scale * 2 end end local noteHeadChar = { Whole='i', Half='j', Other='k' } local accidentalChar = { ['']='', ['#']='d', ['n']='e', ['b']='f', ['x']='g', ['v']='h' } local rests = { Whole = { text='l', offset=0 }, Half = { text='m', offset=0 }, ['4th'] = { text='n', offset=0 }, ['8th'] = { text='o', offset=1 }, ['16th'] = { text='p', offset=-1 }, ['32nd'] = { text='q', offset=-1 }, ['64th'] = { text='r', offset=-3 } } local trebleChars = { Treble = { text='a', offset=-0.2 }, Bass = { text='b', offset=-0.2 }, Alto = { text='c', offset=-0.2 }, Tenor = { text='c', offset= 1.8 }, Percussion = { text=']', offset=-0.2 }, } local function splitPitchPos(pitchPos) --// nwc.debug('splitPitchPos(pitchPos)', pitchPos) return string.match(pitchPos, '([#bnxv]?)([-]?%d+)([oxXzyYabcdefgh])(%^?)') end local function asOrdinal(i) return i % 10 == 1 and i % 100 ~= 11 and i .. 'st' or i % 10 == 2 and i % 100 ~= 12 and i .. 'nd' or i % 10 == 3 and i % 100 ~= 13 and i .. 'rd' or i .. 'th' end local function dbg(item) return item and item:objType()..'@'..item:indexOffset() or 'item==null' end local function isNoteOrChord(item) return item:objType() == "Note" or item:objType() == "Chord" or item:objType() == "RestChord" end local typeMatches = { Note2Note = true, Note2Chord = true, Note2RestChord = true, Chord2Chord = true, Chord2RestChord = true, RestChord2Chord = true, RestChord2RestChord = true, Rest2Note = true, Rest2Rest = true, Rest2RestChord = true, TempoVariance2Bar = true, } local function typeMatch(grace, item) return grace:objType() == item:objType() or typeMatches[grace:objType() .. '2' .. item:objType()] end local typeMatches = { Note2Note = true, Note2Chord = true, Note2RestChord = true, Chord2Chord = true, Chord2RestChord = true, RestChord2Chord = true, RestChord2RestChord = true, Rest2Note = true, Rest2Rest = true, Rest2RestChord = true, } local function noteRestMatch(grace, item) if typeMatches[grace:objType() .. '2' .. item:objType()] and grace:noteDurBase() == item:noteDurBase() and grace:noteCount() <= item:noteCount() and grace:isTriplet() == item:isTriplet() then for i = 1, grace:noteCount() do if grace:noteDurBase(i) ~= item:noteDurBase(i) then --// nwc.debug('no match', dbg(grace), grace:noteDurBase(i), dbg(item), item:noteDurBase(i)) return false end end return true else --// nwc.debug('no match', dbg(grace), dbg(item), grace:noteDurBase(), item:noteDurBase(), grace:noteCount(), item:noteCount() ) return false end end local function drawStem(item, topY, bottomY, dir) local tipX, tipY = item:xyStemTip(dir) if tipY then if tipY < topY[dir] then nwcdraw.line(tipX, topY[dir], tipX, tipY) end if tipY > bottomY[dir] then nwcdraw.line(tipX, bottomY[dir], tipX, tipY) end end end local function drawLegerLine(stemX, scale, headOnRight, p) nwcdraw.line(stemX - scale * (headOnRight and 0.5 or 1.5), p * scale, stemX + scale * (headOnRight and 1.3 or 0.5), p * scale) end local function drawChord(item, grace, scale, fontsize, yBottom, yTop) if grace:objType() == 'Rest' then local pos = item:objProp('VertOffset') or 0 local r = rests[grace:noteDurBase()] local x, y = item:xyStemAnchor(), (pos + r.offset) * scale nwcdraw.alignText('baseline', 'right') nwcdraw.moveTo(x, y) --// nwc.debug('rest', dbg(grace), grace:noteDurBase()) nwcdraw.text(r.text) local dots = grace:isDotted() if dots then nwcdraw.moveTo(x + 0.8 * scale, y + (pos % 2 == 0 and scale or 0)) nwcdraw.text(dots == 1 and 'z' or 'zz') end else local topY = { [-1] = -math.huge, [1] = -math.huge } local bottomY = { [-1] = math.huge, [1] = math.huge } for i = 1, grace:noteCount() do -- draw noteheads local acc, pos, head, tie = splitPitchPos(grace:notePitchPos(i)) --// nwc.debug(dbg(item), 'aa', grace:notePitchPos(i), pos, acc) local stemDir = item:stemDir(i) local stemX = item:xyStemAnchor() local y = pos * scale topY[stemDir] = math.max(topY[stemDir], y) bottomY[stemDir] = math.min(bottomY[stemDir], y) local headOnRight = stemX - item:xyNoteHead(i) < .5 nwcdraw.alignText('baseline', headOnRight and 'left' or 'right') nwcdraw.moveTo(stemX, y) nwcdraw.text(noteHeadChar[item:durationBase(i)] or noteHeadChar.Other) -- draw dots local dots = grace:isDotted(i) if dots then nwcdraw.moveTo(stemX + (headOnRight and 1.5 or 0.8) * scale, y + (pos % 2 == 0 and scale or 0)) nwcdraw.text(dots == 1 and 'z' or 'zz') end -- draw leger lines for p = 6, pos, 2 do drawLegerLine(stemX, scale, headOnRight, p) end for p = -6, pos, -2 do drawLegerLine(stemX, scale, headOnRight, p) end -- draw accidentals if acc ~= '' then -- TODO - TOO SIMPLE local dx = i % 2 == 1 and -0.4 or -1.1 nwcdraw.moveTo(dx * scale + item:xyStemAnchor() + scale * (headOnRight and -1 or -1), pos * scale) nwcdraw.text(accidentalChar[acc]) end end -- draw stem drawStem(item, topY, bottomY, -1) drawStem(item, topY, bottomY, 1) -- draw articulations -- TODO: STILL MISSING end end local mirror = { -- match: nil or function(grace, item) -- tooFar: nil or function(item) -- draw = function(item, grace/null, scale, fontsize) Clef = { match = typeMatch, draw = function(item, null, scale, fontsize, yBottom, yTop) local c = trebleChars[item:objProp('Type')] nwcdraw.alignText('middle', 'center') nwcdraw.moveTo(item:xyAnchor(), c.offset * scale) nwcdraw.text(c.text) end }, Key = { match = typeMatch, draw = function(item, null, scale, fontsize, yBottom, yTop) end }, TimeSig = { match = typeMatch, draw = function(item, null, scale, fontsize, yBottom, yTop) end }, Bar = { match = typeMatch, draw = function(item, null, scale, fontsize, yBottom, yTop) nwcdraw.moveTo(item:xyAnchor(), 0) nwcdraw.barSegment(item:objProp('Style'), yBottom, yTop) end }, Note = { match = noteRestMatch, tooFar = isNoteOrChord, draw = drawChord }, Chord = { match = noteRestMatch, tooFar = isNoteOrChord, draw = drawChord }, RestChord = { match = noteRestMatch, tooFar = isNoteOrChord, draw = function(item, grace, scale, fontsize) drawChord(item, scale, fontsize) if grace:RestVisible________() then drawRest(item, scale, fontsize) end end }, } local currentGrace = nwcdraw.user.new() local currentItem = nwcdraw.user.new() local firstItem = nwcdraw.user.new() local lastItem = nwcdraw.user.new() local function isCueGrace(item) if item:objType() ~= 'TempoVariance' then --// nwc.debug('iGC',dbg(item),item:isGrace(),item:isHidden()) return item:isGrace() and item:isHidden() else return item:objProp('Style') ~= 'Fermata' end end local function asText(item) return item:durationBase() and item:durationBase():lower() .. ' ' .. item:objType():lower() or item:objType():lower() end local function _draw(t) --// nwc.debug('-----------------------------------------------', t.Pos, dbg(firstItem)) -- currentItem:reset() -- currentGrace:reset() local lines = 5 -- take from staff local yBottom = -t.Scale * (lines - 1) local yTop = t.Scale * (lines - 1) local graceCt = 0 --// nwc.debug('cI1=', dbg(currentItem)) while currentItem:find('next') and isCueGrace(currentItem) do --// nwc.debug('cI2=', dbg(currentItem)) graceCt = graceCt + 1 end --// nwc.debug('cI3=', dbg(currentItem)) if currentItem:objType() ~= 'TempoVariance' then currentItem:find('prior') end firstItem:find(currentItem) --// nwc.debug('start=', dbg(currentItem)) --// nwc.debug('fI=', dbg(firstItem)) local fontsize = nwcdraw.getFontSize() nwcdraw.setFontClass('StaffSymbols') nwcdraw.setFontSize(fontsize * t.Scale * 2) local distance = 0 local graceI = 0 while currentGrace:find('next') and isCueGrace(currentGrace) do graceI = graceI + 1 while true do local type = currentItem:find('next') and currentItem:objType() distance = distance + 1 --// nwc.debug(dbg(currentItem), dbg(currentGrace), distance, graceCt) if mirror[type] and mirror[type].match and mirror[type].match(currentGrace, currentItem) then mirror[type].draw(currentItem, currentGrace, t.Scale, fontsize, yBottom, yTop) break -- continue with next grace elseif mirror[type] and mirror[type].tooFar and mirror[type].tooFar(currentItem) then print('no matching item found for ' .. asOrdinal(graceI) .. ' grace ' .. asText(currentGrace) .. ', instead found ' .. asText(currentItem)) return elseif distance > 3 * graceCt then print('no matching item found for ' .. asOrdinal(graceI) .. ' grace ' .. currentGrace:objType()) return elseif mirror[type] and mirror[type].draw then --// nwc.debug(dbg(currentItem)) mirror[type].draw(currentItem, nil, t.Scale, fontsize, yBottom, yTop) end end end lastItem:find(currentItem) local _, penWidth = nwcdraw.getMicrons(1, t.PenWidth*0.2) local x1 = firstItem:xyAnchor() - t.Overhang local x2 = lastItem:xyRight() + t.Overhang nwcdraw.setPen(t.Pen, penWidth) mirrorStaffLines(t.Scale, x1, x2, lines) ---// nwc.debug('(yBottom, yTop)', yBottom, yTop) nwcdraw.setPen('dash', penWidth) if t.DashLeft then nwcdraw.line(x1, 4, x1, yBottom) end if t.DashBars then currentItem:find(firstItem) while currentItem:find('next', 'bar') and currentItem <= lastItem do local x, y = currentItem:xyAnchor() nwcdraw.line(x, y + 4, x, yBottom) nwcdraw.line(x, y - 4, x, yTop) end end if t.DashRight then nwcdraw.line(x2, 4, x2, yBottom) end end return { ----- nwcut = {['Change Highlight of staff to white & back'] = 'ClipText'}, spec = _spec, create = _create, audit = _audit, spin = _spin, draw = _draw, }