3
* This file is part of jReality. jReality is open source software, made
4
* available under a BSD license:
6
* Copyright (c) 2003-2006, jReality Group: Charles Gunn, Tim Hoffmann, Markus
7
* Schmies, Steffen Weissmann.
11
* Redistribution and use in source and binary forms, with or without
12
* modification, are permitted provided that the following conditions are met:
14
* - Redistributions of source code must retain the above copyright notice, this
15
* list of conditions and the following disclaimer.
17
* - Redistributions in binary form must reproduce the above copyright notice,
18
* this list of conditions and the following disclaimer in the documentation
19
* and/or other materials provided with the distribution.
21
* - Neither the name of jReality nor the names of its contributors nor the
22
* names of their associated organizations may be used to endorse or promote
23
* products derived from this software without specific prior written
26
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
27
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
28
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
29
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
30
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
31
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
32
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
33
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
34
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
35
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
36
* POSSIBILITY OF SUCH DAMAGE.
41
package opencog.spacetime.control;
43
import java.util.Collection;
44
import java.util.Collections;
45
import java.util.HashMap;
46
import java.util.HashSet;
47
import java.util.Iterator;
48
import java.util.LinkedList;
49
import java.util.List;
51
import java.util.WeakHashMap;
52
import java.util.Map.Entry;
54
import opencog.math.Rn;
55
import opencog.spacetime.control.AxisState;
56
import opencog.spacetime.control.InputSlot;
57
import opencog.spacetime.control.Tool;
58
import opencog.spacetime.control.ToolContext;
59
import opencog.spacetime.control.config.ToolSystemConfiguration;
60
import opencog.spacetime.control.tool.AnimatorTool;
61
import opencog.spacetime.space.Geometry;
62
import opencog.spacetime.space.SpaceComponent;
63
import opencog.spacetime.space.SpaceNode;
64
import opencog.spacetime.space.SpacePath;
65
import opencog.spacetime.space.Viewer;
66
import opencog.spacetime.space.data.DoubleArray;
67
import opencog.spacetime.space.pick.AABBPickSystem;
68
import opencog.spacetime.space.pick.PickResult;
69
import opencog.spacetime.space.pick.PickSystem;
70
import opencog.spacetime.space.pick.PosWHitFilter;
71
import opencog.spacetime.util.Input;
72
import opencog.spacetime.util.LoggingSystem;
73
import opencog.spacetime.util.RenderTrigger;
74
import opencog.spacetime.util.Secure;
75
import opencog.spacetime.util.SystemProperties;
85
public class ToolSystem implements ToolEventReceiver {
87
static WeakHashMap<Viewer, ToolSystem> globalTable = new WeakHashMap<Viewer, ToolSystem>();
90
* If <i>v</i> has a tool system already associated to it, return it. Otherwise allocate a default one
94
public static ToolSystem toolSystemForViewer(Viewer v) {
95
synchronized (globalTable) {
96
ToolSystem sm = (ToolSystem) globalTable.get(v);
97
if (sm != null) return sm;
98
LoggingSystem.getLogger(ToolSystem.class).warning("Viewer has no tool system, allocating default");
99
sm = new ToolSystem(v, null, null);
100
globalTable.put(v,sm);
106
* This method just looks up and returns the possibly null toolsystem associated to viewer
110
public static ToolSystem getToolSystemForViewer(Viewer v) {
111
synchronized (globalTable) {
112
ToolSystem sm = (ToolSystem) globalTable.get(v);
117
public static void setToolSystemForViewer(Viewer v, ToolSystem ts) {
118
synchronized (globalTable) {
119
ToolSystem sm = (ToolSystem) globalTable.get(v);
120
if (sm != null) throw new IllegalStateException("Viewer already has tool system "+sm);
121
globalTable.put(v,ts);
125
private static void unsetToolSystem(ToolSystem ts) {
126
synchronized (globalTable) {
127
for (Iterator<Entry<Viewer, ToolSystem>> it = globalTable.entrySet().iterator(); it.hasNext(); ) {
128
Entry<Viewer, ToolSystem> e = it.next();
129
if (e.getValue() == ts) it.remove();
134
private RenderTrigger renderTrigger;
136
protected final LinkedList<ToolEvent> compQueue = new LinkedList<ToolEvent>();
138
private final LinkedList<ToolEvent> triggerQueue = new LinkedList<ToolEvent>();
139
private final HashMap<Tool, List<SpacePath>> toolToPath = new HashMap<Tool, List<SpacePath>>();
140
private List<PickResult> pickResults = Collections.emptyList();
141
private PosWHitFilter hitFilter;
143
private SpacePath emptyPickPath=new SpacePath();
145
protected Viewer viewer;
146
private ToolContextImpl toolContext;
147
protected DeviceManager deviceManager;
148
private ToolManager toolManager;
149
private SlotManager slotManager;
150
private PickSystem pickSystem;
151
private ToolUpdateProxy updater;
152
private ToolEventQueue eventQueue;
153
ToolSystemConfiguration config;
154
private PickResult pickResult;
155
private static InputSlot pointerSlot = InputSlot.getDevice("PointerTransformation");
157
protected boolean executing;
159
private final Object KEY=new Object();
161
private class ToolContextImpl implements ToolContext {
163
InputSlot sourceSlot;
165
private SpacePath rootToLocal;
166
private SpacePath rootToToolComponent;
167
private Tool currentTool;
171
public Viewer getViewer() {
175
public InputSlot getSource() {
176
return event.getInputSlot();
179
public DoubleArray getTransformationMatrix(InputSlot slot) {
180
return deviceManager.getTransformationMatrix(slot);
183
public AxisState getAxisState(InputSlot slot) {
184
return deviceManager.getAxisState(slot);
187
public long getTime() {
188
return event.getTimeStamp();
191
private void setRootToLocal(SpacePath rootToLocal) {
192
this.rootToLocal = rootToLocal;
193
rootToToolComponent = null;
196
public SpacePath getRootToLocal() {
200
public SpacePath getRootToToolComponent() {
201
if (rootToToolComponent == null) {
202
LinkedList<SpaceNode> list = new LinkedList<SpaceNode>();
203
for (Iterator<SpaceNode> i = rootToLocal.reverseIterator(); i.hasNext(); ) {
204
SpaceNode cp = i.next();
205
if (!(cp instanceof SpaceComponent))
207
if (((SpaceComponent) cp).getTools().contains(currentTool)) {
210
list.addFirst((SpaceNode) i.next());
213
rootToToolComponent = SpacePath.fromList(list);
215
return rootToToolComponent;
218
public PickResult getCurrentPick() {
219
if (pickResult == null) {
221
pickResult = pickResults.isEmpty() ? null : (PickResult) pickResults.get(0);
226
public List<PickResult> getCurrentPicks() {
227
if (pickResults == null) {
233
private void setCurrentTool(Tool currentTool) {
234
this.currentTool = currentTool;
237
public void reject() {
241
boolean isRejected() {
245
public SpacePath getAvatarPath() {
246
return ToolSystem.this.getAvatarPath();
249
public PickSystem getPickSystem() {
250
return ToolSystem.this.getPickSystem();
253
public Object getKey() {
258
private static ToolSystemConfiguration loadConfiguration() {
259
ToolSystemConfiguration config;
261
String toolFile = Secure.getProperty(SystemProperties.TOOL_CONFIG_FILE);
262
config = ToolSystemConfiguration.loadConfiguration(
263
Input.getInput(toolFile)
265
LoggingSystem.getLogger(ToolSystem.class).config("Using toolconfig="+toolFile);
266
} catch (Exception e1) {
267
config = ToolSystemConfiguration.loadDefaultConfiguration();
273
* @param viewer the viewer
274
* @param config the config
275
* @param renderTrigger a rendertrigger to synch or null - the ToolSystem does not take care of
276
* setting/removing the triggers viewer and scene root (on initialize/dispose)
278
public ToolSystem(Viewer viewer, ToolSystemConfiguration config, RenderTrigger renderTrigger) {
279
toolContext = new ToolContextImpl();
280
toolManager = new ToolManager();
281
eventQueue = new ToolEventQueue(this);
282
if (config == null) config = loadConfiguration();
283
this.config = config;
284
this.viewer = viewer;
285
deviceManager = new DeviceManager(config, eventQueue, viewer);
286
slotManager = new SlotManager(config);
287
updater = new ToolUpdateProxy(this);
288
this.renderTrigger = renderTrigger;
289
// this code moved over from the ToolSystemViewer constructor
290
setPickSystem(new AABBPickSystem());
291
// provide a reasonable default empty pick path
292
emptyPickPath = new SpacePath();
293
emptyPickPath.push(viewer.getSceneRoot());
296
private class MouseOverSupport implements Tool {
298
List<InputSlot> activation = Collections.emptyList(); //Collections.singletonList(InputSlot.getDevice("EnablePointerHit"));
299
List<InputSlot> pointer = Collections.singletonList(InputSlot.getDevice("PointerTransformation"));
300
InputSlot trigger = InputSlot.getDevice("PointerHit");
306
void mouseOverToolAdded() {
308
//System.out.println("Enabling mouse over support");
309
addToolImpl(this, rootPath);
314
void mouseOverToolRemoved() {
317
//System.out.println("Disabling mouse over support");
318
removeToolImpl(this, rootPath);
322
private MouseOverSupport(SpacePath root) {
323
rootPath=new SpacePath(root);
326
public void activate(ToolContext tc) {
329
public void deactivate(ToolContext tc) {
332
public List<InputSlot> getActivationSlots() {
336
public List<InputSlot> getCurrentSlots() {
340
public String getDescription(InputSlot slot) {
344
public String getDescription() {
345
return "dummy tool to enable mouse over";
348
boolean hasHit=false;
349
SpacePath lastPath=null;
351
public void perform(ToolContext tc) {
356
SpacePath newP = null;
357
boolean hits = false;
358
PickResult p = tc.getCurrentPick();
359
if (p!=null && p.getPickPath() != null && p.getPickPath().getLastElement() instanceof Geometry) {
360
newP = new SpacePath(p.getPickPath());
373
if (lastPath.isEqual(newP)) {
374
// same hit, nothing to do
380
// then fire hit again
392
private void fireNoMoreHit() {
393
eventQueue.addEvent(new ToolEvent(this, deviceManager.getSystemTime(), trigger, AxisState.ORIGIN));
396
private void fireHit() {
397
eventQueue.addEvent(new ToolEvent(this, deviceManager.getSystemTime(), trigger, AxisState.PRESSED));
401
public int hashCode() {
406
public boolean equals(Object obj) {
411
if (getClass() != obj.getClass())
413
throw new IllegalStateException("Duplicate MouseOverSupport!");
418
private boolean initialized;
420
private MouseOverSupport mouseOverSupport;
421
public void initializeSceneTools() {
423
LoggingSystem.getLogger(this).warning("already initialized!");
427
toolManager.cleanUp();
430
SpacePath rootPath = new SpacePath();
431
rootPath.push(viewer.getSceneRoot());
432
addTool(AnimatorTool.getInstanceImpl(KEY), rootPath);
434
// enable mouse over support
435
mouseOverSupport = new MouseOverSupport(rootPath);
437
if (emptyPickPath.getLength() == 0) {
438
emptyPickPath.push(viewer.getSceneRoot());
440
if (pickSystem != null) {
441
pickSystem.setSceneRoot(viewer.getSceneRoot());
443
updater.setSceneRoot(viewer.getSceneRoot());
447
public void processToolEvent(ToolEvent event) {
448
synchronized (mutex) {
449
if (disposed) return;
452
compQueue.add(event);
456
processComputationalQueue();
457
processTriggerQueue();
458
List<ToolEvent> l = deviceManager.updateImplicitDevices();
459
if (l.isEmpty()) break;
461
if (iterCnt > 5000) {
462
//throw new IllegalStateException("recursion in tool system!");
463
LoggingSystem.getLogger(this).warning("may be stuck in endless loop");
467
// handle newly added/removed tools
468
synchronized (mutex) {
469
if (!toolsChanging.isEmpty()) {
470
final List<Pair> l = new LinkedList<Pair>(toolsChanging);
471
toolsChanging.clear();
472
for (Iterator<Pair> i = l.iterator(); i.hasNext(); ) {
476
addToolImpl(p.tool, p.path);
478
removeToolImpl(p.tool, p.path);
484
if (event.getInputSlot() == InputSlot.getDevice("SystemTime")) {
485
deviceManager.setSystemTime(event.getTimeStamp());
486
if (renderTrigger != null) {
487
renderTrigger.finishCollect();
488
renderTrigger.startCollect();
493
protected void processComputationalQueue() {
494
while (!compQueue.isEmpty()) {
495
ToolEvent event = (ToolEvent) compQueue.removeFirst();
496
deviceManager.evaluateEvent(event, compQueue);
497
if (isTrigger(event) && !event.isConsumed()) triggerQueue.add(event);
501
private boolean isTrigger(ToolEvent event) {
502
InputSlot slot = event.getInputSlot();
503
boolean ret = slotManager.isActiveSlot(slot) || slotManager.isActivationSlot(slot);
507
protected void processTriggerQueue() {
508
if (triggerQueue.isEmpty()) return;
510
HashSet<Tool> activatedTools = new HashSet<Tool>();
511
HashSet<Tool> deactivatedTools = new HashSet<Tool>();
512
HashSet<Tool> stillActiveTools = new HashSet<Tool>();
514
SpacePath pickPath = null;
516
for (ToolEvent event : triggerQueue) { //Iterator iter = triggerQueue.iterator(); iter.hasNext();) {
517
// ToolEvent event = (ToolEvent) iter.next();
518
toolContext.event = event;
519
InputSlot slot = event.getInputSlot();
520
toolContext.sourceSlot = slot;
524
AxisState axis = deviceManager.getAxisState(slot);
526
boolean noTrigger = true;
528
if (axis != null && axis.isPressed()) { // possible activation:
530
Set<Tool> candidatesForPick = new HashSet<Tool>(slotManager.getToolsActivatedBySlot(slot));
532
Set<Tool> candidates = new HashSet<Tool>();
534
// TODO: see if activating more than one Tool for an axis
536
for (Tool candidate : candidatesForPick) {
537
if (!toolManager.needsPick(candidate)) //throw new Error();
538
LoggingSystem.getLogger(this).warning("Something wrong with pick candidates\n");
540
if (!candidatesForPick.isEmpty()) {
541
// now we need a pick path
542
if (pickPath == null)
543
pickPath = calculatePickPath();
544
int level = pickPath.getLength();
546
Collection<Tool> selection = toolManager.selectToolsForPath(pickPath, level--, candidatesForPick);
547
if (selection.isEmpty()) continue;
548
LoggingSystem.getLogger(this).finer("selected pick tools:" + selection);
549
for (Tool tool : selection) {
550
registerActivePathForTool(pickPath, tool);
552
candidates.addAll(selection);
553
// now all Tools in the candidates list need to be
554
// processed=activated
555
activateToolSet(candidates);
556
} while (candidates.isEmpty() && level > 0);
557
activatedTools.addAll(candidates);
558
noTrigger = candidates.isEmpty();
561
if (axis != null && axis.isReleased()) { // possible deactivation
562
Set<Tool> deactivated = findDeactivatedTools(slot);
563
deactivatedTools.addAll(deactivated);
564
deactivateToolSet(deactivated);
565
noTrigger = deactivated.isEmpty();
568
// process all active tools NEW: only if no tool was (de)activated
569
if (noTrigger) { //activatedTools.isEmpty() && deactivatedTools.isEmpty()
570
Set<Tool> active = slotManager.getActiveToolsForSlot(slot);
571
stillActiveTools.addAll(active);
572
processToolSet(active);
575
triggerQueue.clear();
576
// NEW: this is now obsolete
577
// // don't update used slots for deactivated tools!
578
// stillActiveTools.removeAll(deactivatedTools);
579
slotManager.updateMaps(stillActiveTools, activatedTools, deactivatedTools);
582
private void registerActivePathForTool(SpacePath pickPath, Tool tool) {
583
List<SpacePath> ap = Collections.singletonList(
584
pickPath.getLastElement() instanceof Geometry ? pickPath.popNew() : pickPath
586
toolToPath.put(tool, ap);
589
private double[] pointerTrafo = new double[16];
591
private double[] currentPointer = new double[16];
593
protected final Object mutex=new Object();
595
private SpacePath avatarPath;
597
private boolean disposed;
599
private void performPick() {
600
if (pickSystem == null) {
601
pickResults = Collections.emptyList();
604
pointerSlot = InputSlot.getDevice("PointerTransformation");
605
currentPointer = deviceManager.getTransformationMatrix(pointerSlot).toDoubleArray(currentPointer);
606
//if (Rn.equals(pointerTrafo, currentPointer)) return;
607
Rn.copy(pointerTrafo, currentPointer);
608
double[] to = new double[] { -pointerTrafo[2], -pointerTrafo[6],
609
-pointerTrafo[10], -pointerTrafo[14] };
610
double[] from = new double[] { pointerTrafo[3], pointerTrafo[7],
611
pointerTrafo[11], pointerTrafo[15] };
613
AABBPickSystem.getFrustumInterval(from, to, viewer);
614
} catch (IllegalStateException e) {
617
pickResults = pickSystem.computePick(from, to);
618
if (!SystemProperties.isPortal) {
619
if (hitFilter == null)
620
hitFilter = new PosWHitFilter(viewer);
622
// throw out pick results whose NDC w-coordinate is negative
623
AABBPickSystem.filterList(hitFilter, from, to, pickResults);
625
// if (pickResults.size() != 0) {
626
// System.err.println(SystemProperties.hostname+" Got "+pickResults.size()+" picks");
627
// System.err.println(SystemProperties.hostname+" picked "+pickResults.get(0).getPickPath().getLastComponent().getName());
631
private SpacePath calculatePickPath() {
633
if (pickResults.isEmpty()) {
634
return emptyPickPath;
636
PickResult result = (PickResult) pickResults.get(0);
637
LoggingSystem.getLogger(this).fine("ToolSystem.calculatePickPath() <HIT>");
638
return result.getPickPath();
642
* calls perform(ToolContext tc) for all tools in the given Set
643
* removes Tools from the set if the tool rejected the activation.
646
* @return false if the current level of tools rejected the context...
648
private void activateToolSet(Set<Tool> toolSet) {
649
for (Iterator<Tool> iter = toolSet.iterator(); iter.hasNext();) {
650
Tool tool = iter.next();
651
toolContext.setCurrentTool(tool);
652
toolContext.event.device=slotManager.resolveSlotForTool(tool, toolContext.sourceSlot);
653
if (toolContext.event.device == null) {
654
LoggingSystem.getLogger(this).warning("activate: resolving "+toolContext.sourceSlot+" failed: "+tool.getClass().getName());
656
for (SpacePath path : getActivePathsForTool(tool)) {
657
toolContext.setRootToLocal(path);
658
tool.activate(toolContext);
659
if (toolContext.isRejected()) {
661
toolContext.rejected=false;
668
* calls perform(ToolContext tc) for all tools in the given Set
672
private void processToolSet(Set<Tool> toolSet) {
673
for (Tool tool : toolSet) {
674
toolContext.setCurrentTool(tool);
675
toolContext.event.device=slotManager.resolveSlotForTool(tool, toolContext.sourceSlot);
676
for (SpacePath path : getActivePathsForTool(tool)) {
677
toolContext.setRootToLocal(path);
678
tool.perform(toolContext);
683
private List<SpacePath> getActivePathsForTool(Tool tool) {
684
List<SpacePath> l = toolToPath.get(tool);
685
if (l == null) return Collections.emptyList();
690
* calls perform(ToolContext tc) for all tools in the given Set
694
private void deactivateToolSet(Set<Tool> toolSet) {
695
for (Tool tool : toolSet) {
696
toolContext.setCurrentTool(tool);
697
toolContext.event.device=slotManager.resolveSlotForTool(tool, toolContext.sourceSlot);
698
if (toolContext.event.device == null) {
699
LoggingSystem.getLogger(this).warning("deavtivate: resolving "+toolContext.sourceSlot+" failed: "+tool.getClass().getName());
701
for (SpacePath path : getActivePathsForTool(tool)) {
702
toolContext.setRootToLocal(path);
703
tool.deactivate(toolContext);
709
* the given slot must have AxisState.isReleased() == true
710
* this is garanteed in processTrigger...
715
private Set<Tool> findDeactivatedTools(InputSlot slot) {
716
return slotManager.getToolsDeactivatedBySlot(slot);
719
public void setPickSystem(PickSystem pickSystem) {
720
this.pickSystem = pickSystem;
721
if (pickSystem != null) {
722
pickSystem.setSceneRoot(viewer.getSceneRoot());
726
public PickSystem getPickSystem() {
730
public void setAvatarPath(SpacePath p) {
732
deviceManager.setAvatarPath(avatarPath);
735
public SpacePath getAvatarPath() {
736
return avatarPath != null ? avatarPath : viewer.getCameraPath();
739
public void dispose() {
740
synchronized (mutex) {
745
} catch (InterruptedException e) {
746
// TODO Auto-generated catch block
750
System.out.println("event queue shut down done...");
751
deviceManager.dispose();
752
eventQueue.dispose();
754
AnimatorTool.disposeInstance(KEY);
755
unsetToolSystem(this); // remove from the viewer->tool-system table
758
final List<Pair> toolsChanging = new LinkedList<Pair>();
760
protected static class Pair {
762
final SpacePath path;
764
Pair(Tool tool, SpacePath path, boolean added) {
771
void addTool(Tool tool, SpacePath path) {
772
synchronized (mutex) {
773
if (disposed) return;
775
toolsChanging.add(new Pair(tool, path, true));
776
else addToolImpl(tool, path);
780
void removeTool(Tool tool, SpacePath path) {
781
synchronized (mutex) {
782
if (disposed) return;
784
toolsChanging.add(new Pair(tool, path, false));
785
else removeToolImpl(tool, path);
794
void addToolImpl(Tool tool, SpacePath path) {
795
boolean first = toolManager.addTool(tool, path);
796
if (!toolManager.needsPick(tool)) {
797
List<SpacePath> l = toolToPath.get(tool);
799
l = new LinkedList<SpacePath>();
800
toolToPath.put(tool, l);
804
} catch (UnsupportedOperationException e) {
805
System.out.println("try adding to sigleton: "+tool);
809
slotManager.registerTool(tool);
810
if (tool.getActivationSlots().contains(InputSlot.POINTER_HIT)) {
811
mouseOverSupport.mouseOverToolAdded();
814
LoggingSystem.getLogger(this).info(
815
"first=" + first + " tool=" + tool + " path=" + path);
823
void removeToolImpl(Tool tool, SpacePath path) {
824
boolean last = toolManager.removeTool(tool, path);
825
for (SpacePath activePath : getActivePathsForTool(tool)) {
826
if (path.isEqual(activePath)) {
827
ToolEvent te = new ToolEvent(this, -1, InputSlot.getDevice("remove"), null, null);
828
toolContext.setCurrentTool(tool);
829
toolContext.setRootToLocal(path);
830
toolContext.event = te;
831
tool.deactivate(toolContext);
832
toolToPath.remove(tool);
836
slotManager.unregisterTool(tool);
837
if (tool.getActivationSlots().contains(InputSlot.POINTER_HIT)) {
838
mouseOverSupport.mouseOverToolRemoved();
841
LoggingSystem.getLogger(this).info(
842
"last=" + last + " tool=" + tool + " path=" + path);
845
public SpacePath getEmptyPickPath() {
846
return emptyPickPath;
849
public void setEmptyPickPath(SpacePath emptyPickPath) {
850
if (emptyPickPath != null) {
851
if (emptyPickPath.getFirstElement().getName() != viewer.getSceneRoot().getName())
852
throw new IllegalArgumentException("empty pick path must start at scene root!");
853
if (emptyPickPath.getFirstElement() != viewer.getSceneRoot()) {
854
LoggingSystem.getLogger(this).warning("Strange situation: same names but different scene roots");
856
this.emptyPickPath = emptyPickPath;
858
this.emptyPickPath = new SpacePath();
859
this.emptyPickPath.push(viewer.getSceneRoot());
863
public RenderTrigger getRenderTrigger() {
864
return renderTrigger;
867
public Object getKey() {