From bf5511442e5c6d72a5ba2aa1df464734a93a23ef Mon Sep 17 00:00:00 2001 From: Mark Slee Date: Wed, 12 Jun 2013 15:40:54 -0700 Subject: [PATCH] Add a mapping tool for Trip to deal with hardware issues --- TestPatterns.pde | 119 ++++++++++++ _Internals.pde | 37 +++- _Overlay.pde | 479 ++++++++++++++++++++++++++++++++++------------- code/GLucose.jar | Bin 14626 -> 14713 bytes 4 files changed, 494 insertions(+), 141 deletions(-) diff --git a/TestPatterns.pde b/TestPatterns.pde index 2b44a93..e7951e3 100644 --- a/TestPatterns.pde +++ b/TestPatterns.pde @@ -133,3 +133,122 @@ class TestProjectionPattern extends SCPattern { } } } + +class MappingTool extends SCPattern { + + private int cubeIndex = 0; + private int stripIndex = 0; + + public boolean mappingModeSingleCube = true; + + public final int CUBE_MODE_ALL = 0; + public final int CUBE_MODE_SINGLE_STRIP = 1; + public final int CUBE_MODE_STRIP_PATTERN = 2; + public int cubeMode = CUBE_MODE_ALL; + + public boolean channelModeRed = true; + public boolean channelModeGreen = false; + public boolean channelModeBlue = false; + + MappingTool(GLucose glucose) { + super(glucose); + } + + private void printInfo() { + println("Cube:" + cubeIndex + " Strip:" + (stripIndex+1)); + } + + public void cube(int delta) { + int len = model.cubes.size(); + cubeIndex = (len + cubeIndex + delta) % len; + printInfo(); + } + + public void strip(int delta) { + int len = Cube.CLIPS_PER_CUBE * Clip.STRIPS_PER_CLIP; + stripIndex = (len + stripIndex + delta) % len; + printInfo(); + } + + public void run(int deltaMs) { + color off = color(0, 0, 0); + color c = off; + color r = #FF0000; + color g = #00FF00; + color b = #0000FF; + if (channelModeRed) c |= r; + if (channelModeGreen) c |= g; + if (channelModeBlue) c |= b; + + int ci = 0; + for (Cube cube : model.cubes) { + if (!mappingModeSingleCube || (cubeIndex == ci)) { + if (cubeMode == CUBE_MODE_STRIP_PATTERN) { + int si = 0; + color sc = off; + for (Strip strip : cube.strips) { + int clipI = si / Clip.STRIPS_PER_CLIP; + switch (clipI) { + case 0: sc = r; break; + case 1: sc = g; break; + case 2: sc = b; break; + case 3: sc = r|g|b; break; + } + if (si % Clip.STRIPS_PER_CLIP == 2) { + sc = r|g; + } + setColor(strip, sc); + ++si; + } + } else if (cubeMode == CUBE_MODE_SINGLE_STRIP) { + setColor(cube, off); + setColor(cube.strips.get(stripIndex), c); + } else { + setColor(cube, c); + } + } else { + setColor(cube, off); + } + ++ci; + } + + } + + public void incCube() { + cubeIndex = (cubeIndex + 1) % model.cubes.size(); + } + + public void decCube() { + --cubeIndex; + if (cubeIndex < 0) { + cubeIndex += model.cubes.size(); + } + } + + public void incStrip() { + int stripsPerCube = Cube.CLIPS_PER_CUBE * Clip.STRIPS_PER_CLIP; + stripIndex = (stripIndex + 1) % stripsPerCube; + } + + public void decStrip() { + int stripsPerCube = Cube.CLIPS_PER_CUBE * Clip.STRIPS_PER_CLIP; + --stripIndex; + if (stripIndex < 0) { + stripIndex += stripsPerCube; + } + } + + public void keyPressed() { + switch (keyCode) { + case UP: incCube(); break; + case DOWN: decCube(); break; + case LEFT: decStrip(); break; + case RIGHT: incStrip(); break; + } + switch (key) { + case 'r': channelModeRed = !channelModeRed; break; + case 'g': channelModeGreen = !channelModeGreen; break; + case 'b': channelModeBlue = !channelModeBlue; break; + } + } +} diff --git a/_Internals.pde b/_Internals.pde index 8dc1aae..29b0dde 100644 --- a/_Internals.pde +++ b/_Internals.pde @@ -38,12 +38,16 @@ final int TARGET_FRAMERATE = 45; int startMillis, lastMillis; GLucose glucose; HeronLX lx; +MappingTool mappingTool; LXPattern[] patterns; LXTransition[] transitions; LXEffect[] effects; OverlayUI ui; +ControlUI controlUI; +MappingUI mappingUI; PandaDriver pandaFront; PandaDriver pandaRear; +boolean mappingMode = false; boolean pandaBoardsEnabled = false; @@ -64,6 +68,7 @@ void setup() { // Set the patterns glucose.lx.setPatterns(patterns = patterns(glucose)); + mappingTool = new MappingTool(glucose); logTime("Built patterns"); glucose.lx.addEffects(effects = effects(glucose)); logTime("Built effects"); @@ -79,11 +84,12 @@ void setup() { logTime("Build PandaDriver"); // Build overlay UI - ui = new OverlayUI(); + ui = controlUI = new ControlUI(); + mappingUI = new MappingUI(mappingTool); logTime("Built overlay UI"); // MIDI devices - SCMidiDevices.initializeStandardDevices(glucose, ui.patternKnobs, ui.transitionKnobs, ui.effectKnobs); + SCMidiDevices.initializeStandardDevices(glucose, controlUI.patternKnobs, controlUI.transitionKnobs, controlUI.effectKnobs); logTime("Setup MIDI devices"); println("Total setup: " + (millis() - startMillis) + "ms"); @@ -118,9 +124,31 @@ void drawUI() { } boolean uiOn = true; -boolean knobsOn = true; +int restoreToIndex = -1; + void keyPressed() { + if (mappingMode) { + mappingTool.keyPressed(); + } switch (key) { + case 'm': + mappingMode = !mappingMode; + if (mappingMode) { + LXPattern pattern = lx.getPattern(); + for (int i = 0; i < patterns.length; ++i) { + if (pattern == patterns[i]) { + restoreToIndex = i; + break; + } + } + ui = mappingUI; + lx.setPatterns(new LXPattern[] { mappingTool }); + } else { + ui = controlUI; + lx.setPatterns(patterns); + lx.goIndex(restoreToIndex); + } + break; case 'p': pandaBoardsEnabled = !pandaBoardsEnabled; println("PandaBoard Output: " + (pandaBoardsEnabled ? "ON" : "OFF")); @@ -128,9 +156,6 @@ void keyPressed() { case 'u': uiOn = !uiOn; break; - case 'k': - knobsOn = !knobsOn; - break; } } diff --git a/_Overlay.pde b/_Overlay.pde index acf8505..244b891 100644 --- a/_Overlay.pde +++ b/_Overlay.pde @@ -14,30 +14,153 @@ import java.lang.reflect.*; * into the Processing library once it is stabilized and need not be * regularly modified. */ -class OverlayUI { - - private final PFont titleFont = createFont("Myriad Pro", 10); - private final PFont itemFont = createFont("Lucida Grande", 11); - private final PFont knobFont = titleFont; - private final int w = 140; - private final int leftPos; - private final int leftTextPos; - private final int lineHeight = 20; - private final int sectionSpacing = 12; - private final int controlSpacing = 18; - private final int tempoHeight = 20; - private final int knobSize = 28; - private final float knobIndent = .4; - private final int knobSpacing = 6; - private final int knobLabelHeight = 14; - private final color lightBlue = #666699; - private final color lightGreen = #669966; +abstract class OverlayUI { + protected final PFont titleFont = createFont("Myriad Pro", 10); + protected final color titleColor = #AAAAAA; + protected final PFont itemFont = createFont("Lucida Grande", 11); + protected final PFont knobFont = titleFont; + protected final int w = 140; + protected final int leftPos; + protected final int leftTextPos; + protected final int lineHeight = 20; + protected final int sectionSpacing = 12; + protected final int controlSpacing = 18; + protected final int tempoHeight = 20; + protected final int knobSize = 28; + protected final float knobIndent = .4; + protected final int knobSpacing = 6; + protected final int knobLabelHeight = 14; + protected final color lightBlue = #666699; + protected final color lightGreen = #669966; + private PImage logo; + + protected final int STATE_DEFAULT = 0; + protected final int STATE_ACTIVE = 1; + protected final int STATE_PENDING = 2; + + protected OverlayUI() { + leftPos = width - w; + leftTextPos = leftPos + 4; + logo = loadImage("logo-sm.png"); + } + + protected void drawLogoAndBackground() { + image(logo, 4, 4); + stroke(color(0, 0, 100)); + // fill(color(0, 0, 50, 50)); // alpha is bad for perf + fill(color(0, 0, 30)); + rect(leftPos-1, -1, w+2, height+2); + } + + protected void drawToggleTip(String s) { + fill(#999999); + textFont(itemFont); + textAlign(LEFT); + text(s, leftTextPos, height-6); + } + + protected void drawHelpTip() { + textFont(itemFont); + textAlign(RIGHT); + text("Tap 'u' to restore UI", width-4, height-6); + } + + public void drawFPS() { + textFont(titleFont); + textAlign(LEFT); + fill(#666666); + text("FPS: " + (((int)(frameRate * 10)) / 10.), 4, height-6); + } + + protected int drawObjectList(int yPos, String title, Object[] items, Method stateMethod) { + return drawObjectList(yPos, title, items, classNameArray(items, null), stateMethod); + } + + protected int drawObjectList(int yPos, String title, Object[] items, String[] names, Method stateMethod) { + noStroke(); + fill(titleColor); + textFont(titleFont); + textAlign(LEFT); + text(title, leftTextPos, yPos += lineHeight); + if (items != null) { + textFont(itemFont); + color textColor; + boolean even = true; + for (int i = 0; i < items.length; ++i) { + Object o = items[i]; + int state = STATE_DEFAULT; + try { + state = ((Integer) stateMethod.invoke(this, o)).intValue(); + } catch (Exception x) { + throw new RuntimeException(x); + } + switch (state) { + case STATE_ACTIVE: + fill(lightGreen); + textColor = #eeeeee; + break; + case STATE_PENDING: + fill(lightBlue); + textColor = color(0, 0, 75 + 15*sin(millis()/200.));; + break; + default: + textColor = 0; + fill(even ? #666666 : #777777); + break; + } + rect(leftPos, yPos+6, width, lineHeight); + fill(textColor); + text(names[i], leftTextPos, yPos += lineHeight); + even = !even; + } + } + return yPos; + } + + protected String[] classNameArray(Object[] objects, String suffix) { + if (objects == null) { + return null; + } + String[] names = new String[objects.length]; + for (int i = 0; i < objects.length; ++i) { + names[i] = className(objects[i], suffix); + } + return names; + } + + protected String className(Object p, String suffix) { + String s = p.getClass().getName(); + int li; + if ((li = s.lastIndexOf(".")) > 0) { + s = s.substring(li + 1); + } + if (s.indexOf("SugarCubes$") == 0) { + s = s.substring("SugarCubes$".length()); + } + if ((suffix != null) && ((li = s.indexOf(suffix)) != -1)) { + s = s.substring(0, li); + } + return s; + } + + protected int objectClickIndex(int firstItemY) { + return (mouseY - firstItemY) / lineHeight; + } + + abstract public void draw(); + abstract public void mousePressed(); + abstract public void mouseDragged(); + abstract public void mouseReleased(); +} + +/** + * UI for control of patterns, transitions, effects. + */ +class ControlUI extends OverlayUI { private final String[] patternNames; private final String[] transitionNames; private final String[] effectNames; - - private PImage logo; private int firstPatternY; private int firstPatternKnobY; @@ -62,12 +185,8 @@ class OverlayUI { public final VirtualPatternKnob[] patternKnobs; public final VirtualTransitionKnob[] transitionKnobs; public final VirtualEffectKnob[] effectKnobs; - - OverlayUI() { - leftPos = width - w; - leftTextPos = leftPos + 4; - logo = loadImage("logo-sm.png"); - + + ControlUI() { patternNames = classNameArray(patterns, "Pattern"); transitionNames = classNameArray(transitions, "Transition"); effectNames = classNameArray(effects, "Effect"); @@ -95,22 +214,10 @@ class OverlayUI { throw new RuntimeException(x); } } - - void drawHelpTip() { - textFont(itemFont); - textAlign(RIGHT); - text("Tap 'u' to restore UI", width-4, height-6); - } - - void draw() { - image(logo, 4, 4); - - stroke(color(0, 0, 100)); - // fill(color(0, 0, 50, 50)); // alpha is bad for perf - fill(color(0, 0, 30)); - rect(leftPos-1, -1, w+2, height+2); - - int yPos = 0; + + public void draw() { + drawLogoAndBackground(); + int yPos = 0; firstPatternY = yPos + lineHeight + 6; yPos = drawObjectList(yPos, "PATTERN", patterns, patternNames, patternStateMethod); yPos += controlSpacing; @@ -159,10 +266,7 @@ class OverlayUI { text("" + ((int)(lx.tempo.bpmf() * 100) / 100.), leftPos + w/2., yPos + tempoHeight - 6); yPos += tempoHeight; - fill(#999999); - textFont(itemFont); - textAlign(LEFT); - text("Tap 'u' to hide UI", leftTextPos, height-6); + drawToggleTip("Tap 'u' to hide"); } public LXParameter getOrNull(List items, int index) { @@ -172,17 +276,6 @@ class OverlayUI { return null; } - public void drawFPS() { - textFont(titleFont); - textAlign(LEFT); - fill(#666666); - text("FPS: " + (((int)(frameRate * 10)) / 10.), 4, height-6); - } - - private final int STATE_DEFAULT = 0; - private final int STATE_ACTIVE = 1; - private final int STATE_PENDING = 2; - public int getState(LXPattern p) { if (p == lx.getPattern()) { return STATE_ACTIVE; @@ -209,56 +302,8 @@ class OverlayUI { } return STATE_DEFAULT; } - - protected int drawObjectList(int yPos, String title, Object[] items, Method stateMethod) { - return drawObjectList(yPos, title, items, classNameArray(items, null), stateMethod); - } - - private int drawObjectList(int yPos, String title, Object[] items, String[] names, Method stateMethod) { - noStroke(); - fill(#aaaaaa); - textFont(titleFont); - textAlign(LEFT); - text(title, leftTextPos, yPos += lineHeight); - if (items != null) { - textFont(itemFont); - color textColor; - boolean even = true; - for (int i = 0; i < items.length; ++i) { - Object o = items[i]; - int state = STATE_DEFAULT; - try { - state = ((Integer) stateMethod.invoke(this, o)).intValue(); - } catch (Exception x) { - throw new RuntimeException(x); - } - switch (state) { - case STATE_ACTIVE: - fill(lightGreen); - textColor = #eeeeee; - break; - case STATE_PENDING: - fill(lightBlue); - textColor = color(0, 0, 75 + 15*sin(millis()/200.));; - break; - default: - textColor = 0; - fill(even ? #666666 : #777777); - break; - } - rect(leftPos, yPos+6, width, lineHeight); - fill(textColor); - text(names[i], leftTextPos, yPos += lineHeight); - even = !even; - } - } - return yPos; - } private void drawKnob(int xPos, int yPos, int knobSize, LXParameter knob) { - if (!knobsOn) { - return; - } final float knobValue = knob.getValuef(); String knobLabel = knob.getLabel(); if (knobLabel == null) { @@ -298,33 +343,6 @@ class OverlayUI { textAlign(CENTER); textFont(knobFont); text(knobLabel, xPos + knobSize/2, yPos + knobSize + knobLabelHeight - 2); - - } - - private String[] classNameArray(Object[] objects, String suffix) { - if (objects == null) { - return null; - } - String[] names = new String[objects.length]; - for (int i = 0; i < objects.length; ++i) { - names[i] = className(objects[i], suffix); - } - return names; - } - - private String className(Object p, String suffix) { - String s = p.getClass().getName(); - int li; - if ((li = s.lastIndexOf(".")) > 0) { - s = s.substring(li + 1); - } - if (s.indexOf("SugarCubes$") == 0) { - s = s.substring("SugarCubes$".length()); - } - if ((suffix != null) && ((li = s.indexOf(suffix)) != -1)) { - s = s.substring(0, li); - } - return s; } class VirtualPatternKnob extends LXVirtualParameter { @@ -395,7 +413,7 @@ class OverlayUI { } else if ((mouseY >= firstEffectKnobY) && (mouseY < firstEffectKnobY + knobSize + knobLabelHeight)) { effectKnobIndex = (mouseX - leftTextPos) / (knobSize + knobSpacing); } else if (mouseY > firstEffectY) { - int effectIndex = (mouseY - firstEffectY) / lineHeight; + int effectIndex = objectClickIndex(firstEffectY); if (effectIndex < effects.length) { if (activeEffectIndex == effectIndex) { effects[effectIndex].enable(); @@ -406,7 +424,7 @@ class OverlayUI { } else if ((mouseY >= firstTransitionKnobY) && (mouseY < firstTransitionKnobY + knobSize + knobLabelHeight)) { transitionKnobIndex = (mouseX - leftTextPos) / (knobSize + knobSpacing); } else if (mouseY > firstTransitionY) { - int transitionIndex = (mouseY - firstTransitionY) / lineHeight; + int transitionIndex = objectClickIndex(firstTransitionY); if (transitionIndex < transitions.length) { activeTransitionIndex = transitionIndex; } @@ -416,7 +434,7 @@ class OverlayUI { patternKnobIndex += NUM_PATTERN_KNOBS / 2; } } else if (mouseY > firstPatternY) { - int patternIndex = (mouseY - firstPatternY) / lineHeight; + int patternIndex = objectClickIndex(firstPatternY); if (patternIndex < patterns.length) { patterns[patternIndex].setTransition(transitions[activeTransitionIndex]); lx.goIndex(patternIndex); @@ -449,6 +467,197 @@ class OverlayUI { } +/** + * UI for control of mapping. + */ +class MappingUI extends OverlayUI { + + private MappingTool mappingTool; + + private final String MAPPING_MODE_ALL = "All On"; + private final String MAPPING_MODE_SINGLE_CUBE = "Single Cube"; + private final String[] mappingModes = { + MAPPING_MODE_ALL, + MAPPING_MODE_SINGLE_CUBE, + }; + private final Method mappingModeStateMethod; + + private final String CUBE_MODE_ALL = "All Strips"; + private final String CUBE_MODE_SINGLE_STRIP = "Single Strip"; + private final String CUBE_MODE_STRIP_PATTERN = "Strip Pattern"; + private final String[] cubeModes = { + CUBE_MODE_ALL, + CUBE_MODE_SINGLE_STRIP, + CUBE_MODE_STRIP_PATTERN + }; + private final Method cubeModeStateMethod; + + private final String CHANNEL_MODE_RED = "Red"; + private final String CHANNEL_MODE_GREEN = "Green"; + private final String CHANNEL_MODE_BLUE = "Blue"; + private final String[] channelModes = { + CHANNEL_MODE_RED, + CHANNEL_MODE_GREEN, + CHANNEL_MODE_BLUE, + }; + private final Method channelModeStateMethod; + + private int firstMappingY; + private int firstCubeY; + private int firstChannelY; + private int cubeFieldY; + private int stripFieldY; + + private boolean dragCube; + private boolean dragStrip; + + MappingUI(MappingTool mappingTool) { + this.mappingTool = mappingTool; + try { + mappingModeStateMethod = getClass().getMethod("getMappingState", Object.class); + channelModeStateMethod = getClass().getMethod("getChannelState", Object.class); + cubeModeStateMethod = getClass().getMethod("getCubeState", Object.class); + } catch (Exception x) { + throw new RuntimeException(x); + } + } + + public int getMappingState(Object mappingMode) { + boolean active = (mappingMode == MAPPING_MODE_SINGLE_CUBE) == mappingTool.mappingModeSingleCube; + return active ? STATE_ACTIVE : STATE_DEFAULT; + } + + public int getChannelState(Object channelMode) { + boolean active = false; + if (channelMode == CHANNEL_MODE_RED) { + active = mappingTool.channelModeRed; + } else if (channelMode == CHANNEL_MODE_GREEN) { + active = mappingTool.channelModeGreen; + } else if (channelMode == CHANNEL_MODE_BLUE) { + active = mappingTool.channelModeBlue; + } + return active ? STATE_ACTIVE : STATE_DEFAULT; + } + + public int getCubeState(Object cubeMode) { + boolean active = false; + if (cubeMode == CUBE_MODE_ALL) { + active = mappingTool.cubeMode == mappingTool.CUBE_MODE_ALL; + } else if (cubeMode == CUBE_MODE_SINGLE_STRIP) { + active = mappingTool.cubeMode == mappingTool.CUBE_MODE_SINGLE_STRIP; + } else if (cubeMode == CUBE_MODE_STRIP_PATTERN) { + active = mappingTool.cubeMode == mappingTool.CUBE_MODE_STRIP_PATTERN; + } + return active ? STATE_ACTIVE : STATE_DEFAULT; + } + + public void draw() { + drawLogoAndBackground(); + int yPos = 0; + firstMappingY = yPos + lineHeight + 6; + yPos = drawObjectList(yPos, "MAPPING MODE", mappingModes, mappingModes, mappingModeStateMethod); + yPos += sectionSpacing; + + firstCubeY = yPos + lineHeight + 6; + yPos = drawObjectList(yPos, "CUBE MODE", cubeModes, cubeModes, cubeModeStateMethod); + yPos += sectionSpacing; + + firstChannelY = yPos + lineHeight + 6; + yPos = drawObjectList(yPos, "CHANNELS", channelModes, channelModes, channelModeStateMethod); + yPos += sectionSpacing; + + cubeFieldY = yPos + lineHeight + 6; + yPos = drawValueField(yPos, "CUBE ID", glucose.model.getRawIndexForCube(mappingTool.cubeIndex)); + yPos += sectionSpacing; + + stripFieldY = yPos + lineHeight + 6; + yPos = drawValueField(yPos, "STRIP ID", mappingTool.stripIndex + 1); + + drawToggleTip("Tap 'm' to return"); + } + + private int drawValueField(int yPos, String label, int value) { + yPos += lineHeight; + textAlign(LEFT); + textFont(titleFont); + fill(titleColor); + text(label, leftTextPos, yPos); + fill(0); + yPos += 6; + rect(leftTextPos, yPos, w-8, lineHeight); + yPos += lineHeight; + + fill(#999999); + textAlign(CENTER); + textFont(itemFont); + text("" + value, leftTextPos + (w-8)/2, yPos - 5); + + return yPos; + } + + private int lastY; + + public void mousePressed() { + dragCube = dragStrip = false; + lastY = mouseY; + if (mouseY >= stripFieldY) { + if (mouseY < stripFieldY + lineHeight) { + dragStrip = true; + } + } else if (mouseY >= cubeFieldY) { + if (mouseY < cubeFieldY + lineHeight) { + dragCube = true; + } + } else if (mouseY >= firstChannelY) { + int index = objectClickIndex(firstChannelY); + switch (index) { + case 0: mappingTool.channelModeRed = !mappingTool.channelModeRed; break; + case 1: mappingTool.channelModeGreen = !mappingTool.channelModeGreen; break; + case 2: mappingTool.channelModeBlue = !mappingTool.channelModeBlue; break; + } + } else if (mouseY >= firstCubeY) { + int index = objectClickIndex(firstCubeY); + switch (index) { + case 0: mappingTool.cubeMode = mappingTool.CUBE_MODE_ALL; break; + case 1: mappingTool.cubeMode = mappingTool.CUBE_MODE_SINGLE_STRIP; break; + case 2: mappingTool.cubeMode = mappingTool.CUBE_MODE_STRIP_PATTERN; break; + } + } else if (mouseY >= firstMappingY) { + int index = objectClickIndex(firstMappingY); + if (index < 2) { + mappingTool.mappingModeSingleCube = (index > 0); + } + } + } + + public void mouseReleased() { + } + + public void mouseDragged() { + final int DRAG_THRESHOLD = 5; + int dy = lastY - mouseY; + if (abs(dy) >= DRAG_THRESHOLD) { + lastY = mouseY; + if (dragCube) { + if (dy < 0) { + mappingTool.decCube(); + } else { + mappingTool.incCube(); + } + } else if (dragStrip) { + if (dy < 0) { + mappingTool.decStrip(); + } else { + mappingTool.incStrip(); + } + } + } + + } + + +} + void mousePressed() { if (mouseX > ui.leftPos) { ui.mousePressed(); diff --git a/code/GLucose.jar b/code/GLucose.jar index 7b7d1bd6667c008405c35d539f21ce015d17b723..fdf9ec33188b2f8a7920ddd72181c3641c65f9ef 100644 GIT binary patch delta 2302 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