'From Squeak3.2alpha of 1 November 2001 [latest update: #4554] on 29 November 2001 at 1:31:55 pm'! "Change Set: Genie-UpdateToV202 Date: 15 August 2001 Author: Nathanael Scharli and Stephan Rudlof *** Genie v2.02 (Update from Genie 1.0) *** This changeset updates Genie to version 2.02. The new features are: - More than 10 times faster lookup (with VM plugin) - New VM plugin (by Stephan Rudlof) and optimized image level code - Execute 'CRRecognizer checkPluginVersion' and watch the Transcript output in order to check availability/compatibility of the VM plugin. - Faster graphical feedback (especially imporant for PDAs) - New dictionary format - Old dictionaries are automatically converted to the new format. This convertion includes modification of the parameters 'speed' and 'curvature relevance'. This is necessary to retain the behavior of the dictionaries with the optimized algorithms. - The new format is backwards compatible. This means that new dictionaries can still be used with Genie v1.0. However, the parameters 'speed' and 'curveture relevance' are interpreted slightly different, which may lead to slower recognition or inaccuracies in Genie v1.0. - Improved UI (Improved morphs to inspect/modify gesture dictionaries resp. display properties) - BugFixes IMPORTANT information about the Genie VM plugin The Genie VM plugin makes Genie about 10 to 20 times faster. Especially on slower machines (PDA, etc.), it is really worth using it and it makes Genie much more fun!!!! Testing the plugin To see whether the plugin is available and compatible to the image level code execute 'CRRecognizer checkPluginVersion' and watch the Transcript output. To compare the performance of the primitive to the original performance do the following: 1) Load the gesture dictionary 'http://www.iam.unibe.ch/~schaerli/genie2/GenieTest.ggd' in your image. 2) Execute 'CRStrokeFeature testPrimitivePerformance' and watch the transcript output. 3) Execute 'CRStrokeFeature testOriginalPerformance' and watch the transcript output. 4) Compare the output of steps 2) and 3). The primitive should be about 50 times faster. If you are not sure whether the primitive works correctly, do the following: 1) Load the gesture dictionary 'http://www.iam.unibe.ch/~schaerli/genie2/GenieTest.ggd' in your image. 2) Execute 'CRStrokeFeature testPrimitive' and watch the transcript output. Compiling and installing the plugin The Genie plugin may already be part of your VM (execute 'CRRecognizer checkPluginVersion' to check). If not, you can compile and install it yourself, by doing the following: 1) Generate the C-file by executing 'GeniePlugin translate'. This writes the file 'GeniePlugin.c' into the subdiretory 'GeniePlugin'. 2) Compile the plugin and make the resulting dynamic library available for the VM. (This step is platform dependent. On Windows, you have to get the VM sources, build the plugin (enter 'build GeniePlugin.dll') and copy the resulting file GeniePlugin.dll into the Squeak VM folder). --> For more information about Genie, see the class comment of the class AGenieIntroduction."! !AGenieIntroduction commentStamp: '' prior: 0! This text is also available as comment of the class AGenieIntroduction Genie version: 2.01 NS 8/8/2001 15:18 *** Genie: An introduction *** Genie is a character and gesture recognition system inside Squeak. It is completely configurable and allows you to control everything in Squeak by just using a pen. Contents of this introduction 1) Getting started 1.1) Filing it in 1.2) Genie specific items in the World's help menu 1.3) How to enter gestures with Genie? 1.3.1) Genie's behavior is Morph specific 1.3.2) Focus Genie on a certain Morph 1.3.3) Escaping the recognizer 1.4) Genie and events: How does it work? 2) Gesture dictionaries 2.1) Specifiying and changing the gesture dictionary of a Morph 2.1.1) Name and exported name of a gesture dictionary 2.1.2) Default gesture dictionary of a Morph subclass 2.1.3) Gesture dictionary of a Morph instance 2.2) Inspecting, editing and browsing dictionaries 2.2.1) Browsing a dictionary 2.2.2) Adding new gestures for a character 2.2.3) Adding new characters or editing characters 2.2.4) Dictionary inheritance 2.3) The different character types 3) Display properties 4) Integration into Morphic 4.1) Genie and events: The details 4.2) How a Morph can influence gesture handling 4.3) Information available in a CRGesture 5) Examples and tips 5.1) Available exmaple dictionaries 5.2) Tips on building your own dictionaries 6) Version history 7) The Genie plugin (testing and compiling) 7.1) Testing the plugin 7.2) Compiling and installing the plugin 8) Acknowledgement 1) Getting started 1.1) Filing it in The genie system consists of the following 3 change-sets: a) Genie engine: Contains all the classes involved in recognizing a gesture and looking it up in a dictionary. [Category: Genie-Engine]. b) Genie UI: The UI to configure the gesture recognizer. (Inspect/edit/load/save/copy/delete gesture dictionaries, display properties, etc.) [Category: Genie-UI] c) Genie Integration: This code integrates Genie into Morphic. It consists of system integration code in classes like HandMorph, Morph, TextMorph, MorphicEvent, etc. To get a ready-to-use Genie environment, you have to file in the change-sets in the order a) to c). (It would also be possible to create a Genie environment that has no UI or only reduced integration into the system.) 1.2) Genie specific items in the World's help menu There are 3 items in a World's help menu that are genie specific: - Enable/disable Genie: Enables/disables Genie for the current hand. - Genie character dictionaries: Inspect and edit the named genie character dictionaries. - Choose Genie text dictionary: Select the default dictionary for text input. - Genie display properties: Inspect and edit the named genie display properties. 1.3) How to enter gestures with Genie? This is a little description of how to enter gestures with Genie. Note that Genie is controlled by the red (first) mouse button. All the other mouse buttons have no effect on Genie. Of course, Genie can also be controlled by a pen. (Squeak treats pen operations in the same way as red mouse button operations). 1.3.1) Genie's behavior is Morph specific The behavior of Genie is Morph specific. Every Morph that handles Genie gestures has an associated Gesture dictionary. This dictionary specifies the parameters to capture and lookup gestures. You can start entering a gesture for such a Morph by pressing down the red mouse button somewhere on that Morph. When you release the button, the Morph dispatches the gesture using the associated dictionary. Morphs that don't handle Gesture dictionaries react with the default behavior to mouse down events. You can change the gesture dictionary of any Morph in several ways. See section 2 for more details. 1.3.2) Focus Genie on a certain Morph Some Morphs are very small (small text panes, etc.) and it may not be very handy that every gesture for such a Morph has to start somewhere on this Morph. Therefore, Genie provides the option to focus itself on a certain Morph. This means that all the red button mouse actions are treated as if they would start on that Morph. To focus Genie on a Morph you have to enter the gesture that is associated to the command #switchFocus on that Morph. You can use the same gesture to diasble the focus later. While Genie is focused on a certain Morph, the cursor shape is a dot instead of an arrow. 1.3.3) Escaping the recognizer If a Morph handles Genie gestures a red button mouse down is interpreterd as the start event for capturing a gesture. However, sometimes a user wants to do a traditional red mouse button click or he wants to drag/select with the red mouse button (select text, etc.). This can be done by pressing the red mouse button and hold it down for the 'escape time' without moving the mouse. The 'escape time' is a parameter of the gesture dictionary and can be changed in the dictionary tool (See section 2.2 for more information). Using this feature it is for example possible to move the cursor or select text in a pane that has an associated gesture dictionary. Note: Genie offers commands to do mouse actions with any mouse button. You can find more about that in the following sections. See also section 5.2. 1.4) Genie and events: How does it work? If Genie is enabled for a certain hand, handling of mouse events generated by this hand is slightly modified. Here is a short description of how it works. (For more details see section 4.1): On every mouse down event, the hand determines the target morph (handler) and the event is sent to this Morph. (This part happens in exectly the same way as it does without Genie). If it is a red button event the target morph decides wheter a) it doesn't want to handle gestures This means that the mouse down event gets processed in the usual way and Genie doesn't get involved at all. b) it wants to handle gestures In this case the Morph tells Genie to capture a gesture in according to the parameters of the gesture dictionary that is associated with the Morph. As a consequence, Genie captures all the mouse move events until the next mouse up event occurs. Then it informs the target Morph about the captured gesture and the Morph can take the appropriate action. NOTE: Every gesture dictionary can define an 'escape time'. If the cursor is not moved away from the start position during this time, the recognizer escapes and simulates default mouse events. Like this it is possible to use default mouse events even with a Morph that handles gestures. (There are also other special commands that escape the recognizer and allow simulation of mouse events with any possible mouse button). 2) Gesture dictionaries The main reason for Genie's flexibility are gesture dictionaries. Every Morph that handles gestures has an associated gesture dictionary. This dictionary is used to lookup the characters or actions associated to a certain gesture. In addition, it specifies lots of parameters about the capturing and lookup process. 2.1) Specifiying and changing the gesture dictionary of a Morph There are a lot of different ways how to specify and change the gesture dictionary of a Morph. You can change the dictionary for Morph classes or individual Morph instances. This section tells you how. 2.1.1) Name and exported name of a gesture dictionary Every gesture dictionary has two kinds of names: An (internal) name for descriptive reasons and an exported name in order to access the dictionary from within a Morph. - (Internal) name: This name is just a descriptive name of the dictionary. It has no effect on how and wheter the dictionary is used by certain Morphs. As an example, my personal dictionary with all the 26 letters could be named 'Letters Nathanael'. My dictionary with numbers could be called 'Numbers Nathanael' and my dictionary with letters, numbers, brackets and other special characters for programming could be called 'Programming Nathanael'. - Exported name: This name is the key to associate dictionaries and Morphs. If a dictionary has an exported name XYZ it means that the dictionary can be accessed from any Morph by using this name. Thus, whenever you want a Morph class or instance to use a certain gesture dictionary, you can refer to it by using it's exported name. As an example, you can specify that all the text related Morphs (TextMorph, PluggableTextMorph, etc.) should use the dictionary that is exported under the name 'Text'. This principle of (internal) name and exported name may sound a bit complicated, but it's very useful because it makes it possible to refer to dictionaries in an abstract (indirect) way. Let's for example assume that Peter and Nathanael are sometimes working with the same image and want to use different gesture dictionaries for editing text. (Let's further assume that all the text related Morphs refer to the dictionry that is exported with the name 'Text'). Now, the only thing to do in order to switch from Nathanael to Peter is to open the dictionary browser and to export the dictionary 'Text Peter' (instead of maybe 'Programming Nathanael') under the name 'Text'. In a similar way it's possible to use 'Letters Nathanael' instead of 'Programming Nathanael' if Nathanael only wants to use only letters for the moment. 2.1.2) Default gesture dictionary of a Morph subclass A very easy way to change the gesture dictionary of a Morph subclass is to override the method gestureDictionaryOrName and return the *exported* name of the gesture dictionary that should be used (return the name as a Symbol!!). If this method is not overridden, the following happens per default: Every Morph subclass tries to use a dictionary with an exported name that is equal to the class name. If there is no such dictionary available, it tries to find a dictionary with an exported name that is equal to a superclass name, and uses this one. If there is also no such dictionary available, the Morph subclass doesn't handle gestures. An exception to this rule about the default gesture dictionary of Morphs are the classes that support text editing (TextMorph, PluggableTextMorph, etc): All these classes use per default the gesture dictionary named 'Text'. Example: To use a dictionary for all the instances of the class EllipseMorph, set the exported name of this dictionary to 'EllipseMorph'. Or, if every Morph subclass (that doesn't define a special gesture dictionary) should use a certain dictionary, just export it with the name 'Morph'. 2.1.3) Gesture dictionary of a Morph instance It's also possible to change the gesture dictionary of an individual instance of a Morph subclass. In the red halo menu of every morph, there is an item called 'change gesture dictionary'. It can be used to assign an exported gesture dictionary to the individual Morph instance or to create a new dictionary for this instance. Every Morph instance that has an associated gesture dictionary provides also other options in the red halo menu: - Inspect gesture dictionary: Inspect/edit the associated gesture dictionary - Make own copy of gesture dictionary: Makes a copy of the gesture dictionary that is then only assigned to this individual Morph. (This dictionary doesn't even have an exported name and it is referenced directly by the Morph instance.) Like this, it is possible to modify the gesture dictionary of the individual instance without affecting other Morphs. - Make own sub-dictionary: Similar to the previous item, but instead of copying the associated dictionary, an empty dictionary that inherits from the previously assigned dictionary is created and assigned to this Morph instance. NOTE: As for all the other events, EventHandler can be used to add special behavior to certain Morph instances. (Whenever you assign a gesture dictionary to a Morph instance by using the red halo menu, an EventHandler gets automatically installed.) As soon as there is an EventHandler assigned to a Morph instance, the EventHandler determines the Morph's behavior as far as gesture handling is concerned. This is the reason why Morphs with an associated EventHandler often don't respond to gesture events per default. (They associated event handler just doesn't specify a gesture dictionary.) For more information see section 4. 2.2) Inspecting, editing and browsing dictionaries There are several possibilities to open the tool to inspect/edit dictionaries. You can use the help menu of the World and select the item 'genie gesture dictionaries'. This opens a tool with all the named dictionaries in the system. Every dictionary shows its contents using 5 tabs. Use the 'Basic' tab to modify basic properties of the dictionary. (Have a look at the balloon help text to get more information about the available preferences and options.) The menu of the dictionaries browser (click button labeled 'M') allows you to save or load dictionaries, to browse parent dictionaries, etc. (In addition, dictionaries and display properties can also be loaded from within the Squeak file list. Just select the file name and choos 'load' from the context menu.) Notes: - Every dictionary has its own properties (controlling lookup, etc.) and it's possible to inherit from dictionaries that have completely different properties. Further, the inheritance mechanism of Genie automatically resolves cycles in the parent hierarchy. - All the tools to inspect/edit/browse are built in according to MVC philosophy. So, it's no problem to open several browsers/inspectors on the same dictionary. 2.2.1) Browsing a dictionary To browse the contents of the dictionary, choose either 'Browse' from the menu (button labeled 'M') or select the 'Browse' tab. A browser consists of two nested panes. The outer pane can be used to browse through all the characters definied in the dictionary. The current character is shown in the top left corner of the pane. For every character, the inner pane can be used to browse all the associated gestures. (There is often only one gesture associated to one character.) The leftmost gesture in the inner pane is the gesture that is associated to the character, while the gestures to the right are the most similar gestures in the dictionary. The text above the graphics says what characters are most similar and what's the distance to this characters (the distance is normalized so that 100 is the maximum). Use the menu button (labeled 'M') of the outer pane to change properties (appearance, etc.) of the browser. 2.2.2) Adding new gestures for a character To add a new gesture for an already existing character, click 'New' in the inner pane. This replaces the inner pane by a pane with a purple background. Enter a new gesture by drawing a stroke starting at any point in the purple area. When finished, the new stroke is displayed in the left part of the purple pane. The most similar gestures of the dictionary are shown at the right side and the text at the top informs about the distances to these gestures. (Note: The maximum distance is 100.) Basically it's enough to enter each gesture only once. Thus, you can now hit 'New' to assign the new gesture to the character. However, if a stroke is entered only once, there is a certain probability that it is accidentally not completely entered as it should be. Therefore, Genie allows the user to enter the stroke for a gesture as many times as he wants to. Just start a new stroke in the purple area to enter a new variant. Note the number in the top left corner that indicates how many strokes were entered for the gesture. If there is more than one stroke entered for a new gesture, the avarage of all these strokes is finally associated to the character. The menu (button labeled 'M' in the purple pane) can be used to remove the last entered stroke, or to switch between showing the last entered stroke or the avarage stroke. Further, the menu can be used to assign a hotspot to the stroke. Hit 'Reset' to remove all the previously entered strokes for the new gesture. Important: Don't mix up the possibility to enter the stroke for a single gesture more than once with the possibility to assign more than one gesture to a single character!! In the first case, the user enters the stroke for one and the same gesture more than once to reduce noise that happens per accident. (Once the 'New' button is hit, only the avarage gesture is assigned to the character). In the second case the user assigns more than one (possibly completely different) gestures to one single character). IMPORTANT: If the system fails to recognize a certain character several times, it often helps to enter the gesture again. (Just enter the gesture for the character again. You don't have to delete the gesture(s) that are already assigned to the character). Even if two gestures that are assigned to a character look nearly the same, there can be relevant differences in the way they have been captured. 2.2.3) Adding new characters or editing characters Use the 'New' button in the browser's outer pane to add a new character to the dictionary. You can now enter the new character in the text pane. Use the context menu (yellow mouse button) to choose from a list of characters and predefined commands (This is especially useful if you don't have a keyboard). Note that most of the commands have a descriptive balloon help associated). After you entered the character, the purple pane to enter a new gesture gets automatically opened. Click on the character representation shown in the top left corner of the outer pane to inspect or edit a character. Similar to the add character text pane, there is a context menu available that presents some predefined characters and their meaning. Note that most of the commands also have an associated balloon help. 2.2.4) Dictionary inheritance Gesture dictionaries can inherit from one or more parent dictionaries. Whenever a gesture is looked up in a dictionary with parents, the gesture is compared to all the gestures in all the dictionaries that are accessible using the inheritance hierarchy. (Cycles in the parent hierarchies are automatically resolved). Using the concept of dictionary inheritance a user should never have to define a gesture for a certain character in more than one dictionary. For example, a user can define all the gestures for basic mouse and command key support in a dictionary 'Mouse'. Then he can inherit from this dictionary for all the other dictionaries that should have basic mouse and keyboard support. Further he can define a dictionary 'BasicText' containing all the basic characters. In another dictionary he can enter special programming macros and when he makes this dictionary inheriting from 'BasicText', there is a dictionary containing both text characters and programming macros. Note that dictionaries in an inheritance hierarchy can still have different parameters (like speed, size relevance, etc). Dictionary inheritance is really nice but there is one disadvantage: It takes more time to look up gestures in nested dictionaries than otherwise. So, if performance is an important issue (slow systems, etc), too much inheritance should be avoided. 2.3) The different character types Originally, characters were real characters (one single ascii character), but now, a character is much more general. Basically, there are 3 different types of characters: - Sequence of characters (keystrokes): This category consists of any character sequence not starting with a #. Sequences starting with a # can be entered by enclosing them into ''. (e.g. '#keystrokes' instead of #keystrokes) - Command: A command is basically a symbol. Commands are used to tell the target Morph to do certain actions. There are a lot of predefinied commands available (#inspectLastGesture, #switchCase, #inspectDictionaries, etc.). New commands can be added. Whenever you have to enter/edit a character, there is a context menu available that shows the predfinied commands and more... (click the yellow (2) mouse button to open the context menu) - Code: A Genie character can contain any kind of Squeak code. The format to enter a code character is the following: #Header#Code, whereas Code is general Squeak code and Header is a descriptive string that can be omitted. (Example: '#beep#Smalltalk beep' or just: '##Smalltalk beep'). When the gesture assigned to the code character is recognized, an instance of CRGesture is sent to the target Morph. Then the entered code gets executed with the CRGesture as the receiver. Every CRGesture provides a lot of informations that can be used to take specifi actions. E.g., you can use the message #target to get the target Morph or you can use #coordinates to get the coordinates of the captured gestures, etc. Just have a look at the class CRGesture and the methods in the protocol 'genie-dispatching' of the class Morph and TextMorph. 3) Display properties One of the imortant features of Genie is the platform independence of the gesture dictionaries. This means that all the gsture dictionaries can be created, changed and used on platforms with completely different properties (PC, Laptop, PDA, ...). If a user wants to use Genie on a specific platform, he has to tell the Genie system about the display properties of the platform. There can be many different display properties in a Squeak image, but only one of them is active at a time. To create, delete, inspect, edit or activate/deactivate display properties, select the item 'Genie display properties' from the World's help menu. See the balloon help assigned with the different options for more information. 4) Integration into Morphic This section covers some important issues about how Genie is integrated into Morphic. This may not be very useful for a simple user, but it provides some informations a programmer can use to easily add specific gesture handling behavior to certain Morphs. 4.1) Genie and events: The details Here is a detailed description what happens if Genie is enabled for a certain hand and a red button mouse down event occurs: 1) The first step happens exactly in the same way as if Genie would be disabled. First, the method HandMorph>>handleEvent: gets called with the event as an argument. Here, the listeners get notified and then, the target morph is determined. (This process depends on the state of the hand, the involved Morphs and the used MorphicEventDispatcher. Several methods in HandMorph, Morph and MorphicEventDispatcher are involved here). Once the target Morph is determined, the message #handleMouseDown: with the event as an argument is sent to this Morph. 2) In the method handleMouseDown, the target Morph decides wheter it wants to handle the mouse down event traditionally or wheter it wants to start capturing a gesture. The basic implementation Morph>>handleMouseDown: does that by calling the method Morph>>allowsGestureStart:. This method checks a) wheter the Morph itself wants to handle gestures (method Morph>>handlesGestureStart:) and b) wheter the current state of Genie and the hand allows the start of a gesture (HandMorph>>allowsGestureStart:). If a) and b) are true, the method Morph>>gestureStart: gets called instead of executing the traditional mouse handling code. (The mouse down event gets passed as an argument to all the mentioned methods). 3) The method Morph>>gestureStart: simply invokes the method HandMorph>>gestureStart: and this just forwards the call to the instance variable genieGestureProcessor. (Note: All the Genie related methods in HandMorph (see protocol 'genie') are simply forwarding the calls to the instance variable genieGestureProcessor.) In CRGestureProcessor>>gestureStart: the gesture processor gets prepared to capture a new gesture (create a CRRecognizer instance, etc.). Besides others, the mouseFocus of the current hand is set to the CRGestureProcessor instance in order to get all the mouse events. 4) The CRGestureProcessor catches all the mouse move events and adds their positions to the CRRecognizer instance that is in charge of recognizing the new feature. This is done until the next mouse up event occurs. Then the CRGestureProcessor requests the newly captured feature from the CRRecognizer and then it builds an instance of CRGesture that contains all the information related to the gesture (feature, coordinates of the feature, gesture dictionary, lookup result, target morph, etc.). 5) Now, the CRGestureProcessor askes the target morph wheter it allows preprocessing of gestures (Morph>>allowsGesturePreprocessing). If yes, it performs some preprocessing like updating of the caps lock state if necessary. Then it sends the message #gesture: to the target Morph and passes the CRGesture instance as an argument. 6) From now on it's up to the Morph how to dispatch the gesture. The default behavior implemented in Morph works basically as follows: If the Morph has an associated gesture handler, it lets it handle the gesture. Otherwise it calls the method Morph>>handleGesture: with the CRGesture as an argument. (See next section) 4.2) How a Morph can influence gesture handling Every Morph has several possibilities to influence gesture handling. Note that the involved methods are basically organized in according to the template method design pattern. So, often it's enough to only ovverride a single hook method. a) What events are handled? Every Morph can decide whether it wants to handle gesture or whether it just wants to get traditional mouse events. Related Morph methods: handlesGestureStart:, disableGestures b) What is the CRGestureProcessor allowed to do? The CRGestureProcessor stored in the instance variable genieGestureProcessor of HandMorph is in charge of capturing the gestures for a Morph. The Morph can define what's the CRGestureProcessor allowed to do. Related Morph methods: allowsGestureEscape, allowsGesturePreprocessing c) What gesture dictionary is used? Every Morph can determine which gesture dictionary gets used. Related Morph methods: defaultGestureDictionaryOrName, gestureDictionaryOrName, gestureDictionaryOrName:, gestureDictionary d) How are gesture events dispatched? After a gesture is captured, the HandMorph creates a CRGesture instance containing all the related information and sends it as an argument to the method gesture: of the target morph. Now, the morph can decide how to dispatch the event. Related Morph methods: gesture:, handleGesture: and all the other methods in the protocol 'genie-dispatching' As all the other events, gesture events can be handled by an EventHandler that is associated to a certain Morph instance. See Morph>>onGestureUse:send:to: and the gesture related methods in EventHandler. 4.3) Information available in a CRGesture Instances of CRGesture represent a gesture that was captured and looked up by a certain dictionary. Once a gesture is entirely captured, the CRGestureProcessor builds such an instance and sends it to the method gesture: of the target Morph. The target morph can use all the information provided by this class to take the appropriate action. Besides the basic information about the gesture, CRGesture also contains the coordinates and the hotspot of the captured gesture. The methods in the 'genie-dispatching' protocol of the classes Morph and TextMorph are a good examples of how to use the information provided by CRGesture. 5) Examples and tips 5.1) Available exmaple dictionaries The following example dictionaries are available at http://www.iam.unibe.ch/~schaerli/genie2/. Please note that the gestures in these dictionaries are quite specific to my handwriting and that they may not be very useful for other users. However, they should be a good example to show how you can use and configure Genie. - Basic [No parents] Includes the basic mouse, halo and command/modifier key operations. Further it includes Genie specific operations (inspect last gesture, inspect dictionaries, etc.). Note: This operations definied here are very useful. So, consider building a similar dictionary as a base for your own gesture dictionaries. In particular, a basic gesture dictionary should associate the 'point gesture' (just a click without moving the mouse) to the command 'redClick' which simulates a red mouse click. - BasicText [Parents: 'Basic'] Includes letters, numbers and some special keys (return, backspace, tab, ...) Further there are some special gestures. (E.g., you can change the alignment of text to {left, center, right} by drawing a long vertical line in the {left, center, right} of the pane). - SpecialCharacters [Parents: 'Basic'] Special characters and special keys available on a general keyboard. - ExtendedText [Parents: 'SpecialCharacters' 'BasicText'] Includes all letters, numbers, special characters and special keys available on a general keyboard. - BasicProgramming [Parents: 'ExtendedText'] Some programming gestures (ifTrue: [], ifFalse: [], etc). - MorphColors [No parents] Allows to change the color of the target morph. - BasicWorld [Parents: 'Basic' 'MorphColors'] Includes some gestures to open new Morphs in the World. (Workspace, Browser, EllispeMorph, StarMorph, TextMorph, etc). It also has a gesture to quit Squeak. Note: The gesturs in this dictionary should be size and shape independent. Therefore, some parameters in the 'Basic' resp. 'Advanced' section have been changed!! - BasicMorph [Parents: 'Basic' 'MorphColors'] Includes gestures to delete a Morph. - GraffitiLetters [Parents: 'Basic']. Letters of the Palm Graffiti style. To get a nice demo environment you can do the following" - Export 'BasicText' or ('ExtendedText') as 'Text'. - Export 'BasicWorld' as 'PasteUpMorph'. - Export 'BasicMorph' as 'BasicMorph'. Change the method defaultGestureDictionaryOrName of the classes EllipseMorph and StarMorph to: defaultGestureDictionaryOrName ^ #BasicMorph. Now you can open new Workspaces, Browsers, etc. by drawing directly into the World. Also StarMorph and EllipseMorph can be opened like this. Further StarMorph and EllipseMorph support gesture handling. For example, you can delete them using a gesture. If you want to enable this basic gesture handling for all the Morphs that have no special behavior definied (for example in an Eventhandler), just export 'BasicMorph' as 'Morph'. These examples give a little taste of Genie's possibilities. However, these are very basic examples and there is much more possible. Just adjust the dictionaries and write integration code for the individual Morphs... 5.2) Tips on building your own dictionaries - Use dictionary inheritance to achieve modular dictionaries. Like that, you don't have to define gestures in more than one dictionary. But note that the lookup in dictionary hierarchies takes a bit more time. So, if speed is an important issue you should avoid too big inheritance hierarchies. - If the system fails to recognize a certain character several times, it often helps to enter the gesture for this character again. (Just enter the gesture for the character again. You don't have to delete the gesture(s) that are already assigned to the character). Even if two gestures that are assigned to a character look nearly the same, there can be relevant differences in the way they have been captured. - It's usually a good thing to associate the 'point gesture' (just a click without moving the mouse) to the command 'redClick' which simulates a red mouse click. Like that, you can use mouse clicks as if Genie was disabled. Have a look at the dictionary 'Basic' for other important operations (mouse, halos, etc.). - There are many predefined commands related to mouse, keyboard, halos and often used Genie operations. So, you can define gestures that simluate mouse actions with any button, toggle special keys (caps lock, shift, command key, control key, etc.), bring up halos, inspect the last gesture, open the active gesture dictionary, etc. The context menu of the 'enter a new character' pane shows you the predefined commands. In addition, you can have a look at the example dictionary 'Basic' to see how these commands can be used. - Don't forget that you can associate any Squeak code to a Genie gesture. Note that this code is always executed inside the CRGesture object that is created when you enter the gesture. Thus, you can access all the available CRGesture methods by just using 'self ...'. (E.g. 'self target' refers to the target morph, 'self coordinates' to the global coordinates of the gesture, 'self position' to the hotspot of the gesture', etc.). Have a look at the dictionary 'WorldExample' for some concrete examples. - Note that Genie can be focused on a specific Morph. This means that all the Genie input is redirected to this Morph. This mode is indicated by a mouse cursor with the shape of a dot (instead of an arrow). See section 1.3.2 for details. - The available example dictionaries may not be very general as far as my handwriting is concerned ;). But even if you define your own dictionaries they may give you some ideas of how to use Genie's features. 6) Version history 02/25/2001: Version 1.0 08/08/2001: Version 2.01 - More than 10 times faster lookup - New VM plugin and optimized image level code - Execute 'CRRecognizer checkPluginVersion' and watch the Transcript output in order to check availability/compatibility of the VM plugin. - Faster graphical feedback (especially imporant for PDAs) - New dictionary format - Old dictionaries are automatically converted to the new format. This convertion includes modification of the parameters 'speed' and 'curvature relevance'. This is necessary to retain the behavior of the dictionaries with the optimized algorithms. - The new format is backwards compatible. This means that new dictionaries can still be used with Genie v1.0. However, the parameters 'speed' and 'curveture relevance' are interpreted slightly different, which may lead to slower recognition or inaccuracies in Genie v1.0. - Improved UI (Improved morphs to inspect/modify gesture dictionaries resp. display properties) 7) The Genie plugin The Genie VM plugin (written by Stephan Rudlof) makes Genie about 10 to 20 times faster. Especially on slower machines (PDA, etc.), it is really worth using it and it makes Genie much more fun!!!! 7.1) Testing the plugin To see whether the plugin is available and compatible to the image level code execute 'CRRecognizer checkPluginVersion' and watch the Transcript output. To compare the performance of the primitive to the original performance do the following: 1) Load the gesture dictionary 'http://www.iam.unibe.ch/~schaerli/genie2/GenieTest.ggd' in your image. 2) Execute 'CRStrokeFeature testPrimitivePerformance' and watch the transcript output. 3) Execute 'CRStrokeFeature testOriginalPerformance' and watch the transcript output. 4) Compare the output of steps 2) and 3). The primitive should be about 50 times faster. If you are not sure whether the primitive works correctly, do the following: 1) Load the gesture dictionary 'http://www.iam.unibe.ch/~schaerli/genie2/GenieTest.ggd' in your image. 2) Execute 'CRStrokeFeature testPrimitive' and watch the transcript output. 7.2) Compiling and installing the plugin The Genie plugin may already be part of your VM (execute 'CRRecognizer checkPluginVersion' to check). If not, you can compile and install it yourself, by doing the following: 1) Generate the C-file by executing 'GeniePlugin translate'. This writes the file 'GeniePlugin.c' into the subdiretory 'GeniePlugin'. 2) Compile the plugin and make the resulting dynamic library available for the VM. (This step is platform dependent. On Windows, you have to get the VM sources, build the plugin (enter 'build GeniePlugin.dll') and copy the resulting file GeniePlugin.dll into the Squeak VM folder). 8) Acknowledgement Many thanks to Stephan Rudlof for writing the Genie Plugin. Thanks also to Kevin Fisher for his valuable feedback about using Genie on the iPaq. Finally, thanks to SqC for creating Squeak and giving me the opportunity to work on Genie. I hope you enjoy!! Nathanael ! Model subclass: #CRDictionary instanceVariableNames: 'name dictionary invertedDictionary exportedName parameters capturedPoints maxMultipleDefinitions maxStrokeDistance parents storeParents storedName versionNO ' classVariableNames: 'ExportedInstanceDictionary InstanceBrowser ' poolDictionaries: '' category: 'Genie-Engine'! Object subclass: #CREcho instanceVariableNames: 'minX minY maxX maxY lastPoint color ' classVariableNames: '' poolDictionaries: '' category: 'Genie-Engine'! Object subclass: #CRRecognizer instanceVariableNames: 'dictionary displayProperties points coordinates lastPoint startTime endTime directions directionPoints escapePossible isEchoEnabled echo rasteredPoints currentDisplayProperties lastAddedPoint ' classVariableNames: 'CharacterDictionary ' poolDictionaries: '' category: 'Genie-Engine'! CRFeature subclass: #CRStrokeFeature instanceVariableNames: 'maxStrokeDistance capturedPoints directionVectors extent startPoint relativePoints squaredWeightedLengths posAngleSum negAngleSum absAngleSum endPoint acuteAngleCount acuteAnglePosition originalStartPoint hotspot globalAngles sizeOrSquaredLengths ' classVariableNames: 'RowBase RowInsertRemove RowInsertRemoveCount StrokeReferenceFeature1 ' poolDictionaries: '' category: 'Genie-Engine'! FillInTheBlankMorph subclass: #FillInTheBlankMorphWithDictMenu instanceVariableNames: '' classVariableNames: '' poolDictionaries: '' category: 'Genie-UI'! FillInTheBlankMorphWithDictMenu subclass: #FillInTheBlankMorphWithCharMenu instanceVariableNames: '' classVariableNames: '' poolDictionaries: '' category: 'Genie-UI'! TestInterpreterPlugin subclass: #GeniePlugin instanceVariableNames: '' classVariableNames: '' poolDictionaries: '' category: 'Genie-Engine'! !GeniePlugin commentStamp: '' prior: 0! This plugin implements the functionality of CRStrokeFeature>>sameClassAbsoluteStrokeDistance: aCRFeature forReference: aBoolean . This means that changes there should be mirrored here!! GeniePlugin>>majorNO should be in sync with version number of Genie. ! AlignmentMorph subclass: #PluggableCollectionMorph instanceVariableNames: 'collection collectionKeys currentIndex model collectionOrSelector okaySelector cancelSelector addSelector deleteSelector changeSelector valueMorphSelector menuSelector keyMorphSelector objectToStringSelector releaseSelector valueMorph keyMorph gotoSelector balloonTextSelector ' classVariableNames: '' poolDictionaries: '' category: 'Genie-UI'! !CRAddFeatureMorph methodsFor: 'private' stamp: 'NS 7/5/2001 17:05'! menuAction | menu string items hotspotMenu | menu _ MenuMorph new. string _ showLast ifTrue: ['Show average gesture'] ifFalse: ['Show last entered gesture']. menu add: string target: self selector: #switchShowLastAction. featuresAndStrokeDistances isEmptyOrNil ifFalse: [menu add: 'Remove last gesture' target: self selector: #removeLastAction]. self shownFeature isStroke ifTrue: [ items _ {'start'. 'end'. 'line'. 'top'. 'bottom'. 'left'. 'right'. 'line'. 'topLeft'. 'topRight'. 'bottomLeft'. 'bottomRight' }. hotspotMenu _ MenuMorph new. items do: [:each | each = 'line' ifTrue: [hotspotMenu addLine] ifFalse: [hotspotMenu add: each target: self selector: #setHotspotAction: argument: each asSymbol]]. menu addMorphBack: (MenuItemMorph new contents: 'Hotspot: ', hotspot asString, ' '; arguments: #(); subMenu: hotspotMenu)]. menu popUpInWorld: World. ! ! !CRAddFeatureMorph methodsFor: 'model access' stamp: 'NS 7/5/2001 11:39'! addToActionAskUser: aBoolean | tempString | self avgFeature isEmpty ifTrue: [^ self inform: 'Cannot add empty gesture']. tempString _ (aBoolean or: [self defaultChar isNil]) ifTrue: [| default | default _ self defaultChar isNil ifTrue: [''] ifFalse: [self defaultChar string]. FillInTheBlankMorphWithCharMenu request: 'Please enter character' initialAnswer: default] ifFalse: [self defaultChar string]. tempString isEmptyOrNil ifFalse: [self model addFeature: (self avgFeature hotspot: hotspot) to: (CRChar string: tempString) requestor: self]! ! !CRAddFeatureMorph methodsFor: 'initialize-release' stamp: 'NS 7/5/2001 11:49'! layout | buttons topRow | self color: Color lightRed. self borderColor: Color blue. self borderWidth: 1. self hResizing: #shrinkWrap; vResizing: #shrinkWrap. self listDirection: #topToBottom. self listCentering: #bottomRight. self featureMorph color: Color transparent. featureCountMorph color: Color transparent; setBalloonText: 'Number of variants entered for this gesture'. topRow _ AlignmentMorph newRow color: Color transparent. topRow addMorph: featureCountMorph. buttons _ AlignmentMorph new color: Color transparent. buttons listDirection: #leftToRight; listCentering: #bottomRight. buttons addMorphBack: self menuButton. buttons addTransparentSpacerOfSize: 5@5. buttons addMorphBack: self resetButton. buttons addTransparentSpacerOfSize: 5@5. buttons addMorphBack: self addToDefaultButton. buttons addTransparentSpacerOfSize: 5@5. buttons addMorphBack: self addToXButton. buttons addTransparentSpacerOfSize: 5@5. buttons addMorphBack: self cancelButton. topRow addMorphBack: buttons. self addMorphBack: topRow. self addMorphBack: self featureMorph.! ! !CRAddFeatureMorph methodsFor: 'buttons' stamp: 'NS 7/5/2001 12:04'! addToDefaultButton ^ (self basicButton label: 'Add'; actionSelector: #addToDefaultAction) setBalloonText: 'Assign this gesture to the character'! ! !CRAddFeatureMorph methodsFor: 'buttons' stamp: 'NS 7/5/2001 12:04'! addToXButton ^ (self basicButton label: 'Add to'; actionSelector: #addToXAction) setBalloonText: 'Assign this gesture to an arbitrary character'! ! !CRAddFeatureMorph methodsFor: 'buttons' stamp: 'NS 7/5/2001 17:16'! basicButton ^ SimpleButtonMorph new target: self; color: Color magenta lighter; borderColor: #raised; actWhen: #buttonUp.! ! !CRAddFeatureMorph methodsFor: 'buttons' stamp: 'NS 7/5/2001 12:04'! cancelButton ^ (self basicButton label: 'Cancel'; actionSelector: #cancelAction) setBalloonText: 'Discard this gesture and go back to the dictionary'! ! !CRAddFeatureMorph methodsFor: 'buttons' stamp: 'NS 7/5/2001 12:05'! menuButton ^ (self basicButton label: 'M'; actionSelector: #menuAction; actWhen: #buttonDown) setBalloonText: 'Menu'! ! !CRAddFeatureMorph methodsFor: 'buttons' stamp: 'NS 7/5/2001 12:05'! resetButton ^ (self basicButton label: 'Reset'; actionSelector: #resetAction) setBalloonText: 'Discard the previously entered variants of this gesture'! ! !CRDictionary methodsFor: 'private' stamp: 'NS 7/23/2001 17:37'! getNameAndVersionFromString: aString | pos s n v tempPos | "Find last pattern!!" tempPos _ -1. [tempPos ~= 0] whileTrue: [ pos _ tempPos. tempPos _ aString findString: self class nameVersionSeparationCharacter asString, 'v' startingAt: tempPos + 1]. pos < 1 ifTrue: [pos _ aString size + 1] ifFalse: [s _ aString copyFrom: pos + 2 to: aString size. s detect: [:each | each isDigit not] ifNone: [v _ Number readFrom: s. (v isKindOf: SmallInteger) ifFalse: [v _ nil]]]. v isNil ifTrue: [pos _ aString size + 1]. n _ aString copyFrom: 1 to: pos - 1. ^ Array with: n with: v.! ! !CRDictionary methodsFor: 'private' stamp: 'NS 7/23/2001 16:55'! getNameAndVersionString | s | s _ self name isNil ifTrue: [''] ifFalse: [self name]. self versionNO = 1000 ifTrue: [^ s]. s _ s, self class nameVersionSeparationCharacter asString. s _ s, 'v', self versionNO asString. ^ s! ! !CRDictionary methodsFor: 'private' stamp: 'NS 8/15/2001 19:33'! localLookup: aCRFeature symmetric: aBoolean maxSpeed: speedIntegerOrNil result: aCRLookupResult "Lookup aCRFeature in the local dictionary and store the result in aCRLookupResult. If aBoolean is true, symmetric distances are used for the lookup (means (A dist: B) = (B dist: A)). speedIntegerOrNil determines the maximum speed (0 max accuracy; 100 max. seed)." | speed size distancesAndFeatures maxNormDist minRelevance startEndRelevance angleRelevance acuteAngleRelevance shapeRelevance timeRelevance sizeRelevance strokeRelevance i limitDelta limit f df d minSize neededForMinSize maxVal t | self isEmpty ifTrue: [^ aCRLookupResult]. "Set speed. If speed is nil take speed in assigned preferences" speed _ speedIntegerOrNil isNil ifTrue: [self parameters speedPercentage] ifFalse: [speedIntegerOrNil min: self parameters speedPercentage]. size _ self size. distancesAndFeatures _ Heap new: size. maxNormDist _ CRFeature maxNormDistance. maxVal _ 1000 * maxNormDist + 1. minSize _ aCRLookupResult minSize. neededForMinSize _ (minSize - aCRLookupResult size) max: 0. "Relevances" minRelevance _ 10. startEndRelevance _ self parameters relevancePromilleForPrimary: self parameters startEndRelevance. angleRelevance _ self parameters relevancePromilleForPrimary: self parameters angleRelevance. acuteAngleRelevance _ self parameters relevancePromilleForPrimary: self parameters acuteAngleRelevance. shapeRelevance _ self parameters relevancePromilleForPrimary: self parameters shapeRelevance. timeRelevance _ self parameters relevancePromilleForSecondary: self parameters timeRelevance. sizeRelevance _ self parameters relevancePromilleForSecondary: self parameters sizeRelevance. strokeRelevance _ self parameters relevancePromilleForPrimary: self parameters strokeRelevance. i _ 0. self dictionary keysDo: [:each | d _ 0. i _ i + 1. startEndRelevance >= minRelevance ifTrue: [t _ aCRFeature startEndDistance: each maxNormDist: maxNormDist. d _ (i > neededForMinSize and: [100 * t > ((100 - (speed * 3 // 4)) * maxNormDist)]) ifTrue: [maxVal] ifFalse: [d _ d + (startEndRelevance * t)]]. (angleRelevance >= minRelevance and: [d < maxVal]) ifTrue: [t _ aCRFeature angleDistance: each maxNormDist: maxNormDist. d _ (i > neededForMinSize and: [100 * t > ((100 - (speed * 3 // 4)) * maxNormDist)]) ifTrue: [maxVal] ifFalse: [d _ d + (angleRelevance * t)]]. d < maxVal ifTrue: [acuteAngleRelevance >= minRelevance ifTrue: [d _ d + (acuteAngleRelevance * (aCRFeature acuteAngleDistance: each maxNormDist: maxNormDist))]. shapeRelevance >= minRelevance ifTrue: [d _ d + (shapeRelevance * (aCRFeature shapeDistance: each maxNormDist: maxNormDist))]. timeRelevance >= minRelevance ifTrue: [d _ d + (timeRelevance * (aCRFeature timeDistance: each maxNormDist: maxNormDist))]. sizeRelevance >= minRelevance ifTrue: [d _ d + (sizeRelevance * (aCRFeature sizeDistance: each maxNormDist: maxNormDist))]]. distancesAndFeatures add: d -> each]. limitDelta _ maxNormDist * speed // 333 * strokeRelevance. limit _ aCRLookupResult distanceAt: minSize. limit _ limit isNil ifTrue: [maxVal] ifFalse: [limit * 1000]. i _ 0. [i _ i + 1. i <= size and: [df _ distancesAndFeatures removeFirst. d _ df key. i <= neededForMinSize or: [d + limitDelta <= limit]]] whileTrue: [ f _ df value. t _ aBoolean ifTrue: [aCRFeature symmetricStrokeDistance: f maxNormDist: maxNormDist] ifFalse: [aCRFeature strokeDistance: f maxNormDist: maxNormDist]. d _ d + (strokeRelevance * t). d < limit ifTrue: [aCRLookupResult addFeature: f char: (self dictionary at: f) distance: (d // 1000). i >= neededForMinSize ifTrue: [limit _ (aCRLookupResult distanceAt: minSize) * 1000]]]. ^ aCRLookupResult. ! ! !CRDictionary methodsFor: 'private' stamp: 'NS 7/23/2001 18:07'! versionNO: anIntegerOrNil versionNO _ anIntegerOrNil isNil ifTrue: [1000] ifFalse: [anIntegerOrNil].! ! !CRDictionary methodsFor: 'accessing' stamp: 'NS 7/17/2001 18:09'! fillFeaturesCacheIfNotFull self dictionary keysDo: [:each | each fillCacheIfNotFull].! ! !CRDictionary methodsFor: 'accessing' stamp: 'NS 7/19/2001 15:20'! lookup: aCRFeature "Lookup a feature with the default parameters of this dictionary" | minSize | "In case of alert / reject, we need at least one different character in the result" minSize _ (self parameters isAlertEnabled or: [self parameters isRejectEnabled]) ifTrue: [self totalMaxMultipleDefinitions + 1 max: 3] ifFalse: [3]. ^ self lookup: aCRFeature minResultSize: minSize.! ! !CRDictionary methodsFor: 'accessing' stamp: 'NS 7/19/2001 15:20'! lookup: aCRFeature minResultSize: anInteger ^ self lookup: aCRFeature minResultSize: anInteger symmetric: false! ! !CRDictionary methodsFor: 'accessing' stamp: 'NS 7/19/2001 15:28'! lookup: aCRFeature minResultSize: anInteger symmetric: aBoolean ^ self lookup: aCRFeature minResultSize: anInteger symmetric: aBoolean maxSpeed: nil.! ! !CRDictionary methodsFor: 'accessing' stamp: 'NS 8/8/2001 17:36'! lookup: aCRFeature minResultSize: anInteger symmetric: aBoolean maxSpeed: speedIntegerOrNil | result minSize | minSize _ self size < anInteger ifTrue: [anInteger min: (self indirectParents inject: self size into: [:size :dict | size + dict size])] ifFalse: [anInteger]. result _ CRLookupResult dictionary: self minSize: minSize. self localLookup: aCRFeature symmetric: aBoolean maxSpeed: speedIntegerOrNil result: result. self indirectParents do: [:each | | feature | feature _ (self parameters hasSameCapturing: each parameters) ifTrue: [aCRFeature] ifFalse: [(CRRecognizer dictionary: each) calculateNewFeature: aCRFeature]. each localLookup: feature symmetric: aBoolean maxSpeed: speedIntegerOrNil result: result]. "If the feature was a dot and there is nothing in the dictionary that comes close, we treat it as a red mouse click." ((aCRFeature isKindOf: CRDotFeature) and: [result distance > 80]) ifTrue: [result addFeature: aCRFeature char: (CRChar string: '#redClick') distance: 80]. ^ result.! ! !CRDictionary methodsFor: 'accessing' stamp: 'NS 7/23/2001 18:07'! versionNO ^ versionNO! ! !CRDictionary methodsFor: 'initialize-release' stamp: 'NS 7/20/2001 14:19'! initialize self dictionary: Dictionary new. self invertedDictionary: Dictionary new. self parameters: CRParameters new. self capturedPoints: self class capturedPoints. self name: #'No name'. self exportedName: #''. self maxMultipleDefinitions: 0. self parents: self createParentsCollection. self storeParents: nil. self storedName: nil. self versionNO: self class versionNO.! ! !CRDictionary methodsFor: 'objects from disk' stamp: 'NS 8/8/2001 16:36'! basicReadDataFrom: aDataStream size: varsOnDisk | parentsString sameNameInstance choice text tempParents array nameAndVersion | varsOnDisk ~= 9 ifTrue: [self error: 'Wrong file format']. aDataStream beginReference: self. "Initialize first to always ensure a consistent state" self initialize. "Test wheter there is already a dictionary with the same name. If yes, ask the user what to do" nameAndVersion _ self getNameAndVersionFromString: aDataStream next. self storedName: nameAndVersion first. self versionNO: nameAndVersion second. ((sameNameInstance _ self class name: self storedName) notNil and: [sameNameInstance ~~ self]) ifTrue: [choice _ (PopUpMenu labels: 'use existing dictionary instead load dictionary with modified name replace existing dictionary') startUpWithCaption: '''', self storedName asString , ''' already exists.'. choice = 1 ifTrue: [self name: nil. text _ 'Skipping ''', self storedName asString, ''' ...']. choice = 3 ifTrue: [sameNameInstance replaceBy: self]. choice ~= 1 ifTrue: [self name: self storedName. text _ 'Loading ''', self nameAsString, ''' ...']] ifFalse: [self name: self storedName. text _ 'Loading ''', self nameAsString, ''' ...']. "Load or skip the dictionary depending on the user's choice" text displayProgressAt: Sensor cursorPoint from: 0 to: 20 during: [:bar | bar value: 1. dictionary _ aDataStream next. bar value: 10. capturedPoints _ aDataStream next. bar value: 11. maxMultipleDefinitions _ aDataStream next. bar value: 12. parameters _ aDataStream next. bar value: 15. invertedDictionary _ aDataStream next. bar value: 19. exportedName _ #''. "Don't assign exported name!!" aDataStream next. bar value: 20. parentsString _ aDataStream next]. "Read parent dictionaries, but don't connect them!!" parents _ self createParentsCollection. tempParents _ aDataStream next. "If not skipped: Connect parents" (choice isNil or: [choice > 1]) ifTrue: ["Connect stored parents that have been really reloaded" tempParents do: [:each | each name isEmptyOrNil not ifTrue: [self addParent: each]]. "Connect parents that by name" array _ (self getParentsAndErrorForString: parentsString). array first do: [:new | (self parents detect: [:old | old storedName = new name] ifNone: []) ifNil: [self addParent: new]]. (array second notNil or: [self hasParents]) ifTrue: [ text _ 'Dictionary ''' , self nameAsString, ''':'. self hasParents ifTrue: [ text _ text , ' The following parents have been reconnected: ', self parentsAsString]. array second ifNotNil: [ text _ text , ' The following parents are not available: ' , array second]. self inform: text]].! ! !CRDictionary methodsFor: 'objects from disk' stamp: 'NS 7/20/2001 14:46'! basicStoreDataOn: aDataStream aDataStream beginInstance: self class size: 9. ('Saving ''', self nameAsString , ''' ...') displayProgressAt: Sensor cursorPoint from: 0 to: 20 during: [:bar | aDataStream nextPut: self getNameAndVersionString. bar value: 1. aDataStream nextPut: dictionary. bar value: 10. aDataStream nextPut: capturedPoints. bar value: 11. aDataStream nextPut: maxMultipleDefinitions. bar value: 12. aDataStream nextPut: parameters. bar value: 15. aDataStream nextPut: invertedDictionary. bar value: 19. aDataStream nextPut: exportedName. bar value: 20. aDataStream nextPut: self parentsAsString]. self storeParents ifTrue: [aDataStream nextPut: parents] ifFalse: [aDataStream nextPut: parents class new]. ! ! !CRDictionary methodsFor: 'objects from disk' stamp: 'NS 7/25/2001 16:11'! readDataFrom: aDataStream size: varsOnDisk "Read data and make sure that all the StrokeFeatures use IntegerArray resp. PointArray where possible." self basicReadDataFrom: aDataStream size: varsOnDisk. self updateToVersion2000.! ! !CRDictionary methodsFor: 'update' stamp: 'NS 8/15/2001 20:43'! updateToVersion2000 "Updates all older dictionaries to version 2000 " | oldS | self versionNO >= 2000 ifTrue: [^ self]. oldS _ self parameters speedPercentage. oldS > 50 ifTrue: [self parameters speedPercentage: (oldS - 25 max: 50)]. self parameters acuteAngleRelevance: (self parameters acuteAngleRelevance * 25 // 15 min: 100). self versionNO: 2000. self dictionary keysDo: [:each | (each isKindOf: CRStrokeFeature) ifTrue: [each updateMaxStrokeDistance]].! ! !CRDictionary class methodsFor: 'named instances' stamp: 'NS 7/30/2001 16:55'! addNamesToMenu: aMenu target: anObject selector: aSymbol self allInstances do: [:each | | tempName s | each name ifNotNil: [tempName _ each nameAsString. s _ each exportedName isEmptyOrNil ifTrue: [tempName] ifFalse: [tempName , ' -> ' , each exportedNameAsString]. aMenu add: s target: anObject selector: aSymbol argument: tempName]]. ^ aMenu! ! !CRDictionary class methodsFor: 'named instances' stamp: 'NS 7/30/2001 16:56'! chooseTextDictionary | menu | menu _ MenuMorph entitled: 'Genie: Choose text dictionary'. self addNamesToMenu: menu target: self selector: #setTextDictionary:. menu popUpInWorld. ! ! !CRDictionary class methodsFor: 'named instances' stamp: 'NS 7/30/2001 16:57'! setTextDictionary: aSymbol | d | d _ self name: aSymbol. d ifNotNil: [d exportedName: #Text].! ! !CRDictionary class methodsFor: 'version' stamp: 'NS 7/20/2001 14:19'! versionNO ^ 2000! ! !CRDictionary class methodsFor: 'private' stamp: 'NS 7/23/2001 17:22'! nameVersionSeparationCharacter ^ $-! ! !CRDictionary class methodsFor: 'update' stamp: 'NS 7/25/2001 17:19'! updateToVersion2000 "Updates all older dictionaries to version 2000" self allSubInstancesDo: [:each | each versionNO isNil ifTrue: [each versionNO: (each getNameAndVersionFromString: each nameAsString) second]. each updateToVersion2000].! ! !CRDictionaryBrowser methodsFor: 'initialize-release' stamp: 'NS 7/5/2001 11:21'! newMorph "Create and initialize a new PluggableCollectionMorph which serves as my graphical representation. There can be many graphical representations for one browser objects. However, all the graphical representations share the same layout properties. Create multiple instances of this clss to open multiple browsers with independent layout properties." | m | m _ PluggableCollectionMorph model: self collectionOrSelector: self dictSelector okaySelector: (self hasOkayButton ifTrue: [#okayActionRequestor:]) cancelSelector: nil addSelector: #addCharActionRequestor: deleteSelector: #deleteCharActionRequestor: gotoSelector: #gotoCharActionRequestor: menuSelector: #menuActionRequestor: changeSelector: #changeCharRequestRequestor: valueMorphSelector: #subPaneMorph:requestor: keyMorphSelector: #charKeyMorph:requestor: objectToStringSelector: #objectToString: releaseSelector: nil balloonTextSelector: #balloonText:requestor:. m listDirection: #topToBottom. m layout. ^ m.! ! !CRDictionaryBrowser methodsFor: 'accessing' stamp: 'NS 7/5/2001 11:40'! createMorphForFeature: aCRFeature "Create a morph representing the given featue" | morph items string isEmpty | "Calculate all the shown features. (The given feature and the most similar features in the dictionary)" (isEmpty _ aCRFeature isEmpty) ifTrue: [items _ OrderedCollection new. nextFeatureCount timesRepeat: [items add: (CRLookupItem feature: aCRFeature char: nil distance: 0)]] ifFalse: [items _ self shownResultItemsFor: aCRFeature]. "Create the morph for the given feature" morph _ self createBasicMorphForFeature: aCRFeature. "Iterate through the collection of similar features and add them to the morph" (items size > 0 and: [self areNextFeatureGraphicsShown]) ifTrue: [| tempMorph | tempMorph _ morph. morph _ AlignmentMorph new listDirection: self orientation; color: Color transparent. morph addMorphFront: tempMorph. items do: [:each | | feature | feature _ each isNil ifTrue: [CREmptyFeature new] ifFalse: [each feature]. morph addTransparentSpacerOfSize: (self imageSize // 10 max: 2@2). tempMorph _ self createBasicMorphForFeature: feature. morph addMorphBack: tempMorph]]. string _ isEmpty ifTrue: ['Empty gesture'] ifFalse: [self createStringForItems: items]. string isEmptyOrNil ifTrue: [^ morph]. morph _ AlignmentMorph newColumn addMorphBack: morph; color: Color transparent; listCentering: #topLeft; cellPositioning: #topLeft. morph addMorphFront: (morph transparentSpacerOfSize: (self imageSize // 15 max: 2@2)); addMorphFront: (StringMorph contents: string). ^ morph! ! !CRDictionaryBrowser methodsFor: 'view hooks' stamp: 'NS 7/5/2001 11:29'! balloonText: aSymbol requestor: aPluggableCollectionMorph aSymbol = #addButton ifTrue: [^ 'Add a new character to this dictionary']. aSymbol = #deleteButton ifTrue: [^ 'Delete this character from the dictionary']. aSymbol = #menuButton ifTrue: [^ 'Menu']. aSymbol = #nextButton ifTrue: [^ 'Next character']. aSymbol = #prevButton ifTrue: [^ 'Previous character']. aSymbol = #gotoButton ifTrue: [^ 'Goto/search a character']. ^ nil! ! !CRDictionaryBrowser methodsFor: 'view hooks' stamp: 'NS 7/5/2001 11:51'! charKeyMorph: anObject requestor: aPluggableTextMorph "A view calls this method to obtain the morph that represents the index (key) of the currently displayed character" | morph string | anObject isNil ifTrue: [^ StringMorph contents: 'Empty']. morph _ AlignmentMorph new color: Color white. morph hResizing: #shrinkWrap. string _ anObject headerString. morph _ morph addMorph: (StringMorph contents: string). morph on: #mouseDown send: #renameCharAction:event:sourceMorph: to: self withValue: aPluggableTextMorph. morph setBalloonText: 'Click to edit or inspect'. ^ morph.! ! !CRDictionaryBrowser methodsFor: 'view hooks' stamp: 'NS 7/30/2001 12:24'! menuActionRequestor: aPluggableCollectionMorph "Called to pop up the menu in a view" | menu string | aPluggableCollectionMorph currentValue isNil ifTrue: [^ nil]. menu _ MenuMorph new. menu add: 'Open new browser' target: self dictionary selector: #openBrowser. self dictionary hasParents ifTrue: [menu addMorphFront: (MenuItemMorph new contents: 'Browse parent'; arguments: #(); subMenu: (self dictionary parentMenuSelector: #openBrowser)). menu addMorphFront: (MenuItemMorph new contents: 'Inspect parent'; arguments: #(); subMenu: (self dictionary parentMenuSelector: #openMorph))]. menu addLine. 0 to: 2 do: [:index | string _ index = 0 ifTrue: ['Don''t calculate next chars'] ifFalse: [index = 1 ifTrue: ['Calculate next char'] ifFalse: ['Calculate next two chars']]. self nextFeatureCount ~= index ifTrue: [menu add: string target: aPluggableCollectionMorph selector: #resendMenuAction: argument: {#menuSetNextFeatureCountRequestor:value:. index}]]. menu addLine. self dictionary hasParents ifTrue: [string _ self showDistanceToFeaturesInParents ifTrue: ['Don''t show distances to features in parents'] ifFalse: ['Show distances to features in parents']. menu add: string target: aPluggableCollectionMorph selector: #resendMenuAction: argument: #menuSwitchShowDistanceToFeaturesInParentsRequestor:]. string _ self areNextFeatureGraphicsShown ifTrue: ['Don''t show next char graphical'] ifFalse: ['Show next char graphical']. menu add: string target: aPluggableCollectionMorph selector: #resendMenuAction: argument: #menuSwitchNextFeatureGraphicsShownRequestor:. menu addLine. string _ self relativeSize ifTrue: ['Full feature size'] ifFalse: ['Relative Feature size']. menu add: string target: aPluggableCollectionMorph selector: #resendMenuAction: argument: #menuSwitchRelativeSizeRequestor:. string _ self areCapturedPointsShown ifTrue: ['Don''t show captured points'] ifFalse: ['Show captured points']. menu add: string target: aPluggableCollectionMorph selector: #resendMenuAction: argument: #menuSwitchCapturedPointsShownRequestor:. menu addLine. menu add: 'Change feature graphics size...' target: aPluggableCollectionMorph selector: #resendMenuAction: argument: #menuChangeImageSizeRequestor:. string _ self orientation = #topToBottom ifTrue: ['Horizontal orientation'] ifFalse: ['Vertical orientation']. menu add: string target: aPluggableCollectionMorph selector: #resendMenuAction: argument: #menuSwitchOrientationRequestor:. menu addLine. menu add: 'Reset layout to default' target: aPluggableCollectionMorph selector: #resendMenuAction: argument: #menuSetLayoutToDefaultRequestor:. menu add: 'Save layout as default' target: aPluggableCollectionMorph selector: #resendMenuAction: argument: #menuSaveLayoutAsDefaultRequestor:. ^ menu ! ! !CRDictionaryBrowser methodsFor: 'view hooks' stamp: 'NS 7/5/2001 11:30'! subPaneBalloonText: aSymbol requestor: aPluggableCollectionMorph aSymbol = #addButton ifTrue: [^ 'Add a new gester for this character']. aSymbol = #deleteButton ifTrue: [^ 'Delete this gesture']. aSymbol = #menuButton ifTrue: [^ 'Menu']. aSymbol = #nextButton ifTrue: [^ 'Next gesture']. aSymbol = #prevButton ifTrue: [^ 'Previous gesture']. aSymbol = #gotoButton ifTrue: [^ 'Goto a gesture']. ^ nil! ! !CRDictionaryBrowser methodsFor: 'view hooks' stamp: 'NS 7/5/2001 11:24'! subPaneMorph: anObject requestor: aPluggableCollectionMorph "Called by a feature to retrieve the subPane with all the features" | subPane | subPane _ self subPaneOf: aPluggableCollectionMorph. anObject isNil ifTrue: [subPane ifNotNil: [self removeSubPaneOf: aPluggableCollectionMorph]. ^ StringMorph contents: 'Empty']. (anObject isKindOf: CRAddFeatureMorph) ifTrue: [self subPaneAt: aPluggableCollectionMorph put: anObject. ^ anObject]. (subPane isKindOf: PluggableCollectionMorph) ifFalse: [subPane _ PluggableCollectionMorph model: self collectionOrSelector: anObject okaySelector: nil cancelSelector: nil addSelector: #prepareAddFeatureActionRequestor: deleteSelector: #deleteFeatureActionRequestor: gotoSelector: nil menuSelector: nil changeSelector: nil valueMorphSelector: #featureMorph:requestor: keyMorphSelector: #featureKeyMorph:requestor: objectToStringSelector: #objectToString: releaseSelector: nil balloonTextSelector: #subPaneBalloonText:requestor:. self removeDependent: subPane. self subPaneAt: aPluggableCollectionMorph put: subPane. subPane listDirection: #topToBottom; layout] ifTrue: [subPane collectionOrSelector: anObject]. ^ subPane! ! !CRDictionaryBrowserAppModel methodsFor: 'accessing' stamp: 'NS 7/19/2001 15:28'! lookup: aCRFeature fast: fastBoolean includeParents: aBoolean "Lookup the given feature in the dictionary and update the cache" | size symmetric result | "Determine the minimum size of the lookup result" size _ fastBoolean ifTrue: [3] ifFalse: [self dictionary totalMaxMultipleDefinitions + 1 max: 3]. "Check the cache holding lookup results including parent features" (self resultCacheWithParents includesKey: aCRFeature) ifTrue: [result _ self resultCacheWithParents at: aCRFeature. result size >= size ifTrue: [^ aBoolean ifTrue: [result] ifFalse: [result copyWithoutParentFeatures]]]. "Check the cache holding lookup results without parent features" (aBoolean not and: [self resultCache includesKey: aCRFeature]) ifTrue: [result _ self resultCache at: aCRFeature. result size > size ifTrue: [^ result]]. "Do a real lookup" symmetric _ (self dictionary selfOrIndirectParentIncludesFeature: aCRFeature). result _ self dictionary lookup: aCRFeature minResultSize: size symmetric: symmetric maxSpeed: 0. "If lookup was symmetric ((A dist: B) = (B dist: A)): Update caches" symmetric ifTrue: [ aBoolean ifTrue: [self resultCacheWithParents at: aCRFeature put: result. (self resultCache includesKey: aCRFeature) ifTrue: [self resultCache removeKey: aCRFeature]] ifFalse: [self resultCache at: aCRFeature put: result]]. result. ^ result.! ! !CRDictionaryMorph methodsFor: 'view hooks' stamp: 'NS 7/5/2001 14:47'! editParents | answer | answer _ FillInTheBlankMorphWithDictMenu request: 'Enter the parents (enclose names in '''', '''' = none)' initialAnswer: parents. (answer isEmptyOrNil not and: [answer ~= parents]) ifTrue: [ | array | parents _ answer. array _ model getParentsAndErrorForString: answer. parentCount _ array first size. (answer ~= '''''' and: [array second notNil]) ifTrue: [self inform: 'The following parents are not available: ' , array second]]. self indicateChanged.! ! !CRDictionaryMorph methodsFor: 'view hooks' stamp: 'NS 7/5/2001 16:34'! exportedName ^ exportedName isEmptyOrNil ifTrue: [''] ifFalse: [exportedName] ! ! !CRDictionaryMorph methodsFor: 'submorphs' stamp: 'NS 7/5/2001 12:05'! createAcceptButton ^ (self createBasicButton label: 'Accept'; actionSelector: #acceptAction) setBalloonText: 'Accept the changes'! ! !CRDictionaryMorph methodsFor: 'submorphs' stamp: 'NS 7/5/2001 12:02'! createBasicButton ^ createdSubmorphs add: (SimpleButtonMorph new target: self; borderColor: #raised; actWhen: #buttonUp).! ! !CRDictionaryMorph methodsFor: 'submorphs' stamp: 'NS 7/30/2001 14:11'! createBasicTabMorph | morph labels maxLen items alertRejectText | morph _ self createEmptyTabMorph. labels _ {'Exported name'. 'Name'. 'Direct parents (click to inspect)'. 'Speed (%)'. 'Size relevance (%)'. 'Time relevance (%)'. 'Escape time (ms)'. 'Alert / Reject'. '- Max. distance (0-100)'. '- Min. difference (0-100)'. '- Min. relative difference (%)'}. labels _ labels collect: [:each | self createBasicLabelStringMorph: each]. maxLen _ labels inject: 0 into: [:max :each | max _ max max: each extent x]. alertRejectText _ 'Alert / reject if: distance > max. distance OR (difference < min. difference AND relative difference < min. relative difference). The relative difference means the difference of the best two matches divided by the distance of the best match'. items _ {(self createBasicUpdatingStringMorphOn: self get: #exportedName put: #exportedName:) setBalloonText: 'Dictionaries can be assigned to morphs using their exported name'. (self createBasicUpdatingStringMorphOn: self get: #name put: #name:) setBalloonText: 'Dictionaries are internally identified by their name'. (self createBasicUpdatingStringMorphOn: self get: #parentCount put: nil) setBalloonText: 'Click to inspect / edit the parent dictionaries'; on: #mouseUp send: #editParents to: self; submorphsDo: [:each | each on: #mouseUp send: #editParents to: self]. (self createBasicUpdatingNumberMorphOn: self get: #speedPercentage put: #speedPercentage: min: 0 max: 100) setBalloonText: 'Speed-accuracy tradeoff. 0% = max. accuracy. 100% = max. speed'. (self createBasicUpdatingNumberMorphOn: self get: #sizeRelevance put: #sizeRelevance: min: 0 max: 100) setBalloonText: 'Relevance of the stroke''s absolute size. A value of 100% means that it is as important as the size-independent geometric features. (See Advanced section)'. (self createBasicUpdatingNumberMorphOn: self get: #timeRelevance put: #timeRelevance: min: 0 max: 100) setBalloonText: 'Relevance of the time used to draw the entire stroke. A value of 100% means that it is as important as the size-independent geometric features. (See Advanced section)'. (self createBasicUpdatingNumberMorphOn: self get: #escapeTime put: #escapeTime: min: 0 max: 100000) setBalloonText: 'If you leave the pen at the starting point for this amount of time, the recognizer gets escaped'. AlignmentMorph new addMorphBack: ( self createBasicUpdatingButtonOn: self action: #switchAlert get: #isAlertEnabled); addTransparentSpacerOfSize: 20@0; addMorphBack: ( self createBasicUpdatingButtonOn: self action: #switchReject get: #isRejectEnabled); setBalloonText: alertRejectText maxLineLength: 50. AlignmentMorph new addMorphBack: ( self createBasicUpdatingNumberMorphOn: self get: #alertDistance put: #alertDistance: min: 0 max: 100); addTransparentSpacerOfSize: 20@0; addMorphBack: ( self createBasicUpdatingNumberMorphOn: self get: #rejectDistance put: #rejectDistance: min: 0 max: 100); setBalloonText: alertRejectText maxLineLength: 50. AlignmentMorph new addMorphBack: ( self createBasicUpdatingNumberMorphOn: self get: #alertDistanceDifference put: #alertDistanceDifference: min: 0 max: 100); addTransparentSpacerOfSize: 20@0; addMorphBack: ( self createBasicUpdatingNumberMorphOn: self get: #rejectDistanceDifference put: #rejectDistanceDifference: min: 0 max: 100); setBalloonText: alertRejectText maxLineLength: 50. AlignmentMorph new addMorphBack: ( self createBasicUpdatingNumberMorphOn: self get: #alertRelativeDistanceDifference put: #alertRelativeDistanceDifference: min: 0 max: 100); addTransparentSpacerOfSize: 20@0; addMorphBack: ( self createBasicUpdatingNumberMorphOn: self get: #rejectRelativeDistanceDifference put: #rejectRelativeDistanceDifference: min: 0 max: 100); setBalloonText: alertRejectText maxLineLength: 50}. 1 to: labels size do: [:index | | label item row spacerLen | label _ labels at: index. item _ items at: index. spacerLen _ maxLen - label extent x + 20. row _ AlignmentMorph newRow. row addMorphBack: label. row addTransparentSpacerOfSize: spacerLen @ 0. row addMorphBack: item. morph addMorphBack: row]. morph addTransparentSpacerOfSize: 5 @ 5. morph addMorphBack: self createDefaultButtons. ^ morph.! ! !CRDictionaryMorph methodsFor: 'submorphs' stamp: 'NS 7/5/2001 16:03'! createBasicUpdatingNumberMorphOn: anObject get: getSelector put: putSelector min: minNumber max: maxNumber | morph | morph _ UpdatingNumericStringMorph on: anObject selector: getSelector. morph stepTime: (morph stepTime max: 500). morph putSelector: putSelector; min: minNumber; max: maxNumber. ^ createdSubmorphs add: (self wrapInAlignmentMorph: morph)! ! !CRDictionaryMorph methodsFor: 'submorphs' stamp: 'NS 7/5/2001 16:03'! createBasicUpdatingStringMorphOn: anObject get: getSelector put: putSelector | morph | morph _ UpdatingStringMorph on: anObject selector: getSelector. morph stepTime: (morph stepTime max: 500). morph putSelector: putSelector; useStringFormat. ^ createdSubmorphs add: (self wrapInAlignmentMorph: morph)! ! !CRDictionaryMorph methodsFor: 'submorphs' stamp: 'NS 7/5/2001 12:05'! createCancelButton ^ (self createBasicButton label: 'Cancel'; actionSelector: #cancelAction) setBalloonText: 'Cancel the changes'! ! !CRDictionaryMorph methodsFor: 'submorphs' stamp: 'NS 7/5/2001 12:05'! createCloseButton ^ self createBasicButton label: 'Close'; actionSelector: #closeAction! ! !CRDictionaryMorph methodsFor: 'submorphs' stamp: 'NS 7/30/2001 13:58'! createEmptyTabMorph | morph | morph _ AlignmentMorph newColumn. morph listCentering: #center. ^ createdSubmorphs add: morph! ! !CRDictionaryMorph methodsFor: 'submorphs' stamp: 'NS 7/5/2001 16:02'! wrapInAlignmentMorph: aMorph | outerMorph | outerMorph _ AlignmentMorph new color: Color white. outerMorph hResizing: #shrinkWrap; vResizing: #shrinkWrap. outerMorph addMorph: aMorph. ^ outerMorph.! ! !CRDisplayPropertiesInstanceBrowser methodsFor: 'private' stamp: 'NS 6/28/2000 12:10'! defaultFileName: anObject ^ anObject name , self defaultFileNameSuffix.! ! !CRDisplayPropertiesInstanceBrowser methodsFor: 'view hooks' stamp: 'NS 7/5/2001 11:29'! balloonText: aSymbol requestor: aPluggableCollectionMorph aSymbol = #addButton ifTrue: [^ 'Create a new dictionary']. aSymbol = #deleteButton ifTrue: [^ 'Delete this dictionary']. aSymbol = #menuButton ifTrue: [^ 'Menu']. aSymbol = #nextButton ifTrue: [^ 'Next dictionary']. aSymbol = #prevButton ifTrue: [^ 'Previous dictionary']. aSymbol = #gotoButton ifTrue: [^ 'Goto/search a dictionary']. ^ nil! ! !CRDisplayPropertiesInstanceBrowser methodsFor: 'view hooks' stamp: 'NS 7/5/2001 14:15'! loadActionRequestor: aPluggableCollectionMorph | name file | (self changeRequestor: aPluggableCollectionMorph) ifFalse: [^ self]. file _ StandardFileMenu oldFile. name _ file ifNotNil: [file directory fullNameFor: file name]. name isNil ifFalse: [self loadAndAddFromFileNamed: name requestor: aPluggableCollectionMorph]! ! !CRDisplayPropertiesInstanceBrowser methodsFor: 'initialize-release' stamp: 'NS 7/5/2001 11:27'! newMorph "Create a new morph representing my contents. There can be many morphs representing the same contents without consistency problems" | m | self updateInstanceCollection. m _ PluggableCollectionMorph model: self collectionOrSelector: collection okaySelector: #okayActionRequestor: cancelSelector: nil addSelector: #addActionRequestor: deleteSelector: #deleteActionRequestor: gotoSelector: #gotoActionRequestor: menuSelector: #menuActionRequestor: changeSelector: #changeRequestor: valueMorphSelector: #valueMorph:requestor: keyMorphSelector: #keyMorph:requestor: objectToStringSelector: nil releaseSelector: nil balloonTextSelector: #balloonText:requestor:. m layout. ^ m! ! !CRDictionaryInstanceBrowser methodsFor: 'view hooks' stamp: 'NS 7/30/2001 12:24'! menuActionRequestor: aPluggableCollectionMorph | menu newMenu currentDict | menu _ super menuActionRequestor: aPluggableCollectionMorph. aPluggableCollectionMorph currentValue ifNil: [^ menu]. newMenu _ MenuMorph new. newMenu add: 'Open in browser' target: aPluggableCollectionMorph selector: #resendMenuAction: argument: #browseActionRequestor:. currentDict _ aPluggableCollectionMorph currentValue. currentDict hasParents ifTrue: [newMenu addMorphFront: (MenuItemMorph new contents: 'Browse parent'; arguments: #(); subMenu: (currentDict parentMenuSelector: #openBrowser)). newMenu addMorphFront: (MenuItemMorph new contents: 'Inspect parent'; arguments: #(); subMenu: (currentDict parentMenuSelector: #openMorph))]. menu addMorphFront: MenuLineMorph new. newMenu items do: [:each | menu addMorphFront: each]. ^ menu! ! !CRDisplayPropertiesMorph methodsFor: 'submorphs' stamp: 'NS 7/5/2001 12:06'! createAcceptButton ^ (self createBasicButton label: 'Accept'; actionSelector: #acceptAction) setBalloonText: 'Accept the changes'! ! !CRDisplayPropertiesMorph methodsFor: 'submorphs' stamp: 'NS 7/5/2001 12:03'! createBasicButton ^ createdSubmorphs add: (SimpleButtonMorph new target: self; borderColor: #raised; actWhen: #buttonUp).! ! !CRDisplayPropertiesMorph methodsFor: 'submorphs' stamp: 'NS 7/5/2001 12:06'! createCancelButton ^ (self createBasicButton label: 'Cancel'; actionSelector: #cancelAction) setBalloonText: 'Cancel the changes'! ! !CREcho methodsFor: 'initialize-release' stamp: 'NS 7/26/2001 10:48'! initialize color _ Color blue.! ! !CREcho methodsFor: 'accessing' stamp: 'NS 7/26/2001 11:21'! addPoint: aPoint "We can save some time if we don't open this morph before we know the first point." | x y | lastPoint = aPoint ifTrue: [^ self]. x _ aPoint x. y _ aPoint y. lastPoint ifNil: [lastPoint _ aPoint. minX _ maxX _ x. minY _ maxY _ y. ^ self]. x < minX ifTrue: [minX _ x] ifFalse: [x > maxX ifTrue: [maxX _ x]]. y < minY ifTrue: [minY _ y] ifFalse: [y > maxY ifTrue: [maxY _ y]]. World canvas line: lastPoint to: aPoint width: 2 color: color. lastPoint _ aPoint. ! ! !CREcho methodsFor: 'accessing' stamp: 'NS 7/26/2001 10:44'! delete | m | minX ifNil: [^ self]. m _ Morph new. m position: (minX - 1) @ (minY - 1). m extent: (maxX - minX + 2) @ (maxY - minY + 2). m openInWorld. m delete.! ! !CREcho class methodsFor: 'instance creation' stamp: 'NS 7/26/2001 10:48'! new ^ super new initialize! ! !CRFeature methodsFor: 'private comparing' stamp: 'NS 7/19/2001 15:33'! sameClassAcuteAngleDistance: aCRFeature maxNormDist: maxNumber ^ 0! ! !CRFeature methodsFor: 'private comparing' stamp: 'NS 7/19/2001 15:33'! sameClassAngleDistance: aCRFeature maxNormDist: maxNumber ^ 0! ! !CRFeature methodsFor: 'private comparing' stamp: 'NS 7/19/2001 15:34'! sameClassShapeDistance: aCRFeature maxNormDist: maxNumber ^ 0! ! !CRFeature methodsFor: 'private comparing' stamp: 'NS 7/19/2001 15:34'! sameClassStartEndDistance: aCRFeature maxNormDist: maxNumber ^ 0! ! !CRFeature methodsFor: 'private comparing' stamp: 'NS 7/19/2001 15:34'! sameClassStrokeDistance: aCRFeature maxNormDist: maxNumber ^ 0! ! !CRFeature methodsFor: 'private comparing' stamp: 'NS 7/19/2001 15:34'! sameClassSymmetricStrokeDistance: aCRFeature maxNormDist: maxNumber ^ 0! ! !CRFeature methodsFor: 'comparing' stamp: 'NS 8/15/2001 12:50'! acuteAngleDistance: aCRFeature maxNormDist: maxNumber "This distance measures the difference between two features regading acute angles" ^ aCRFeature class == self class ifTrue: [self sameClassAcuteAngleDistance: aCRFeature maxNormDist: maxNumber] ifFalse: [maxNumber]! ! !CRFeature methodsFor: 'comparing' stamp: 'NS 8/15/2001 12:50'! angleDistance: aCRFeature maxNormDist: maxNumber "This distance measures the difference between two features regading angles" ^ aCRFeature class == self class ifTrue: [self sameClassAngleDistance: aCRFeature maxNormDist: maxNumber] ifFalse: [maxNumber]! ! !CRFeature methodsFor: 'comparing' stamp: 'NS 8/15/2001 12:50'! shapeDistance: aCRFeature maxNormDist: maxNumber "This distance measures the difference between two features regading the shape of the bounding boxes" ^ aCRFeature class == self class ifTrue: [self sameClassShapeDistance: aCRFeature maxNormDist: maxNumber] ifFalse: [maxNumber]! ! !CRFeature methodsFor: 'comparing' stamp: 'NS 7/18/2001 16:36'! sizeDistance: aCRFeature maxNormDist: maxNumber "This distance measures the difference between two features regading the sizes." | mult div s1 s2 | "Calculate the relative size difference and multiply it by 10" s1 _ self size. s2 _ aCRFeature size. s1 > s2 ifTrue: [mult _ s1. div _ s2] ifFalse: [mult _ s2. div _ s1]. mult _ mult - div. div _ div + (self class maxSize // 40). div < 1 ifTrue: [div _ 1]. "max" mult _ 10 * mult // div. "If the ratio is smaller than 8, use a quadratic function to map into the normalized distance space. (8 is mapped to 1/2 of the maximum distance). Otherwise use a linear function. The total function is continuous and 30 (or more) is mapped to the maximum distance" mult > 30 ifTrue: [mult _ 30]. "min" ^ mult < 8 ifTrue: [mult * mult * maxNumber // 128 "(2 * 64)" ] ifFalse: [maxNumber // 2 + (mult - 8 * maxNumber // 44 "(2 * 22)" )] ! ! !CRFeature methodsFor: 'comparing' stamp: 'NS 8/15/2001 12:50'! startEndDistance: aCRFeature maxNormDist: maxNumber "This distance measures the difference between two features regading the relative start- and endpoint." ^ aCRFeature class == self class ifTrue: [self sameClassStartEndDistance: aCRFeature maxNormDist: maxNumber] ifFalse: [maxNumber]! ! !CRFeature methodsFor: 'comparing' stamp: 'NS 8/15/2001 12:50'! strokeDistance: aCRFeature maxNormDist: maxNumber "This distance measures the difference between two features regading the whole stroke." ^ aCRFeature class == self class ifTrue: [self sameClassStrokeDistance: aCRFeature maxNormDist: maxNumber] ifFalse: [maxNumber]! ! !CRFeature methodsFor: 'comparing' stamp: 'NS 8/15/2001 12:50'! symmetricStrokeDistance: aCRFeature maxNormDist: maxNumber "Similar to strokeDistance, but in this case, the distance is symmetric" ^ aCRFeature class == self class ifTrue: [self sameClassSymmetricStrokeDistance: aCRFeature maxNormDist: maxNumber] ifFalse: [maxNumber]! ! !CRFeature methodsFor: 'comparing' stamp: 'NS 7/18/2001 16:36'! timeDistance: aCRFeature maxNormDist: maxNumber "This distance measures the difference between two features regading the time, to user spend while drawing the gesture." | mult div s1 s2 | "Calculate the relative time difference and multiply it by 10" s1 _ self time. s2 _ aCRFeature time. s1 > s2 ifTrue: [mult _ s1. div _ s2] ifFalse: [mult _ s2. div _ s1]. mult _ mult - div. div _ div + 10. mult _ 10 * mult // div. "If the ratio is smaller than 10, use a quadratic function to map into the normalized distance space. (10 is mapped to 1/2 of the maximum distance). Otherwise use a linear function. The total function is continuous and 30 (or more) is mapped to the maximum distance" mult > 30 ifTrue: [mult _ 30]. "min" ^ mult < 10 ifTrue: [mult * mult * maxNumber // 200 "(2 * 100)"] ifFalse: [maxNumber // 2 + (mult - 10 * maxNumber // 40 "(2 * 20)")] ! ! !CRGesture methodsFor: 'lookup result accessing' stamp: 'NS 7/19/2001 15:19'! lookupResult "The lookup result that was generated by looking up the newly captured feature in the dictionary. Usually it contains more than one match. A lot of methods in this class are just foreward methods to the lookup result. See class CRLookupResult (and CRLookupItem, CRChar) for more information" lookupResult isNil ifTrue: [self lookupResult: (self dictionary lookup: self capturedFeature)]. ^ lookupResult.! ! !CRGestureProcessor methodsFor: 'testing' stamp: 'NS 7/6/2001 10:43'! allowsGestureStart: anEvent target: aMorph ^ anEvent isMouse and: [anEvent redButtonPressed and: [(self isEnabled or: [aMorph isKindOf: CRAddFeatureMorph]) and: [self isRecognizing not and: [self isEscaped not]]]]! ! !CRLookupItem methodsFor: 'comparing' stamp: 'NS 8/15/2001 13:15'! < aCRLookupItem ^ self distance < aCRLookupItem distance! ! !CRLookupItem methodsFor: 'comparing' stamp: 'NS 8/15/2001 13:57'! = aCRLookupItem aCRLookupItem == self ifTrue: [^ true]. ^ self distance = aCRLookupItem distance.! ! !CRParameters class methodsFor: 'default parameter' stamp: 'NS 7/19/2001 10:55'! acuteAngleRelevance ^ 25! ! !CRParameters class methodsFor: 'default parameter' stamp: 'NS 8/8/2001 17:23'! escapeTime ^ 250! ! !CRParameters class methodsFor: 'default parameter' stamp: 'NS 8/15/2001 19:37'! speedPercentage ^ 50! ! !CRRecognizer methodsFor: 'private' stamp: 'NS 7/26/2001 15:26'! absMaxCoordDistance: aPoint and: bPoint | x y | x _ (aPoint x - bPoint x). y _ (aPoint y - bPoint y). x < 0 ifTrue: [x _ 0 - x]. y < 0 ifTrue: [y _ 0 - y]. ^ x > y ifTrue: [x] ifFalse: [y]! ! !CRRecognizer methodsFor: 'private' stamp: 'NS 8/8/2001 14:47'! addDirectionTowardsPoint: newPoint minSquaredLength: lenNumber minAngle: angleNumber last: aBoolean dirPointsSize: dirPointsSize directions: directionCollection directionPoints: directionPointCollection "Add the direction (vector) towards newPoint to the sequence of direction vectors and also add newPoint to the sequence of points. The method ensures that all the direction vectors have at least a length of lenNumber that the angle difference to the neighbour vectors is at least angleNumber. NOTE: This method adds new vectors only if they point into another global segment. (E.g., the segment size can be 45 degrees). This GLOBAL view is much better than a relative view (only angle differences are considered) because it is much less sensitive to different start angles or 'noise vectors' in the middle of a stroke!!" | lastDir newDir preLastDir lastP tooShort normLastDir normNewDir dirsSize | dirPointsSize = 0 ifTrue: [directionPointCollection at: 1 put: newPoint. ^ 1]. dirsSize _ dirPointsSize - 1. lastP _ directionPointCollection at: dirPointsSize. newDir _ self directionFrom: lastP to: newPoint. tooShort _ (self squaredDistanceFrom: lastP to: newPoint) < lenNumber. dirsSize = 0 ifTrue: [tooShort ifTrue: [^ 1] ifFalse: [directionCollection at: 1 put: newDir. directionPointCollection at: 2 put: newPoint. ^ 2]]. lastDir _ directionCollection at: dirsSize. normLastDir _ self normalizedDirection: lastDir. normNewDir _ self normalizedDirection: newDir. "If it's not the last vector and the vector points into the same global segment then the last one, replace the endpoint of the last vector" (aBoolean not and: [normLastDir = normNewDir]) ifTrue: [directionCollection at: dirsSize put: (self directionFrom: (directionPointCollection at: dirPointsSize - 1) to: newPoint). directionPointCollection at: dirPointsSize put: newPoint. ^ dirPointsSize]. "If the new vector points into another global segment than the old one, and the vector is long enough, check wheter the new angles have the minimum size. If not: Remove the unnecessary points and make a recursive call" (normLastDir ~= normNewDir and: [dirsSize > 1 and: [tooShort not]]) ifTrue: [preLastDir _ directionCollection at: dirsSize - 1. (self absDirectionDifference: lastDir and: preLastDir) <= angleNumber ifTrue: [self addDirectionTowardsPoint: lastP minSquaredLength: lenNumber minAngle: angleNumber last: false dirPointsSize: dirPointsSize - 2 directions: directionCollection directionPoints: directionPointCollection. ^ self addDirectionTowardsPoint: newPoint minSquaredLength: lenNumber minAngle: angleNumber last: aBoolean dirPointsSize: dirPointsSize - 1 directions: directionCollection directionPoints: directionPointCollection.]]. "If it's the last point: Add the new point in any case. If it would generate a too small angle, remove previous points and make recursive call." (aBoolean and: [tooShort or: [(self absDirectionDifference: newDir and: lastDir) <= angleNumber]]) ifTrue: [^ self addDirectionTowardsPoint: newPoint minSquaredLength: lenNumber minAngle: angleNumber last: aBoolean dirPointsSize: dirPointsSize - 1 directions: directionCollection directionPoints: directionPointCollection.]. "This is the general case: Add the new point and the vector to this point" ^ tooShort ifTrue: [dirPointsSize] ifFalse: [directionCollection at: dirsSize + 1 put: newDir. directionPointCollection at: dirPointsSize + 1 put: newPoint. dirPointsSize + 1].! ! !CRRecognizer methodsFor: 'private' stamp: 'NS 7/26/2001 17:47'! addPointWithoutTest: aPoint "This method adds aPoint to the collection of all the captured points (in global display coordinates). It doesn't test wheter the distance to the last one is big enough" self echo ifNotNil: [self echo addPoint: aPoint]. self lastPoint: aPoint. self lastAddedPoint: aPoint. self points add: aPoint. self coordinates top isNil ifTrue: [self coordinates top: aPoint. self coordinates bottom: aPoint. self coordinates left: aPoint. self coordinates right: aPoint] ifFalse: [ | x y | x _ aPoint x. y _ aPoint y. self coordinates top y > y ifTrue: [self coordinates top: aPoint]. self coordinates bottom y < y ifTrue: [self coordinates bottom: aPoint]. self coordinates left x > x ifTrue: [self coordinates left: aPoint]. self coordinates right x < x ifTrue: [self coordinates right: aPoint]]. ! ! !CRRecognizer methodsFor: 'private' stamp: 'NS 7/26/2001 17:25'! calcNormExtent "Calculates the normalized extent of the stroke" | targetMaxSize | targetMaxSize _ CRFeature maxSize. ^ targetMaxSize * self extent * self currentDisplayProperties aspectRatio // self currentDisplayProperties maxSize min: targetMaxSize @ targetMaxSize max: 1 @ 1! ! !CRRecognizer methodsFor: 'private' stamp: 'NS 7/26/2001 17:25'! calcPointNormDivisor "Calculates the number display points have to be diveded by to get normalized points" | div | div _ self currentDisplayProperties aspectRatio * self extent. ^ (div x max: div y) ceiling.! ! !CRRecognizer methodsFor: 'private' stamp: 'NS 7/26/2001 17:25'! calcPointNormFactor "Calcualtes the number display points have to be multiplied with to get normalized points" ^ CRFeature maxSize * self currentDisplayProperties aspectRatio ! ! !CRRecognizer methodsFor: 'private' stamp: 'NS 7/26/2001 17:42'! clearPoints "Clear all the points collections" self points: OrderedCollection new. self coordinates: CRRecognizerCoordinates new. self lastPoint: nil. self lastAddedPoint: nil.! ! !CRRecognizer methodsFor: 'private' stamp: 'NS 7/26/2001 17:27'! currentDisplayProperties: aCRDisplayProperties currentDisplayProperties _ aCRDisplayProperties! ! !CRRecognizer methodsFor: 'private' stamp: 'NS 7/26/2001 15:50'! directionFrom: startPoint to: endPoint ^ self class degreesX: endPoint x - startPoint x y: endPoint y - startPoint y.! ! !CRRecognizer methodsFor: 'private' stamp: 'NS 7/25/2001 12:20'! directionVectors: directionPointsCollection "Calculat the significant directions based on the siginificant points" | vectors oldP | vectors _ PointArray new: directionPointsCollection size - 1. oldP _ directionPointsCollection at: 1. 2 to: directionPointsCollection size do: [:index | | newOldP | vectors at: index - 1 put: (newOldP _ directionPointsCollection at: index) - oldP. oldP _ newOldP]. ^ vectors! ! !CRRecognizer methodsFor: 'private' stamp: 'NS 7/25/2001 12:23'! ensureMaxDirectionLength: directionPointsCollection size: anInteger "Ensures that all the siginificant directions are not longer than a certain length. I didn't use this method dor a long time and the results were not much worse without it. However, sometimes it's very useful to match a stroke containing one very long direction vector and a very similar stroke containing two shorter vectors with a very small intermdiate angle. Note: This method partitions a vector that is too loong into smaller vectors all of which having exactly the same length. An older version used captured points to introduce new vectors, but it turned out to be worse than this." | oldP maxDistance newDirPoints | maxDistance _ CRFeature maxSize * 8 // 10. maxDistance _ maxDistance * maxDistance. oldP _ directionPointsCollection at: 1. newDirPoints _ OrderedCollection with: oldP. 2 to: anInteger do: [:dirPointsIndex | | p factor | p _ directionPointsCollection at: dirPointsIndex. factor _ (self squaredDistanceFrom: oldP to: p) // maxDistance. factor > 0 ifTrue: [ | part | "Avoid unnecessary root calculations" factor _ factor < 4 ifTrue: [1] ifFalse: [factor sqrt truncated]. part _ p - oldP // (factor + 1). 1 to: factor do: [:index | newDirPoints add: (oldP + (part * index))]]. newDirPoints add: p. oldP _ p. ]. ^ newDirPoints.! ! !CRRecognizer methodsFor: 'private' stamp: 'NS 7/26/2001 17:42'! lastAddedPoint ^ lastAddedPoint! ! !CRRecognizer methodsFor: 'private' stamp: 'NS 7/26/2001 17:42'! lastAddedPoint: aPoint lastAddedPoint _ aPoint! ! !CRRecognizer methodsFor: 'accessing' stamp: 'NS 8/15/2001 19:35'! addPoint: aPoint "Add the given point in global screen coordinates (pixel) to the collection of captured points. The points is only added if the distance to the last one is big enough" | first p | p _ aPoint x asInteger @ aPoint y asInteger. (self escapePossible and: [self points isEmptyOrNil not and: [(self absMaxCoordDistance: p and: (self points at: 1)) >= self currentDisplayProperties minMoveDistance]]) ifTrue: [self escapePossible: false. (self isEchoEnabled and: [self echo isNil]) ifTrue: [self echo: CREcho new. self points do: [:each | self echo addPoint: each]]]. first _ self lastPoint isNil. self lastPoint: p. (first or: [(self absMaxCoordDistance: self lastAddedPoint and: p) >= self currentDisplayProperties minCaptureDistance]) ifTrue: [self addPointWithoutTest: p].! ! !CRRecognizer methodsFor: 'accessing' stamp: 'NS 7/26/2001 17:27'! currentDisplayProperties ^ currentDisplayProperties isNil ifTrue: [self displayProperties] ifFalse: [currentDisplayProperties].! ! !CRRecognizer methodsFor: 'accessing' stamp: 'NS 7/30/2001 12:03'! resetAndStart "Reset the recognizer and start capturing points. This starts the clock measuring how long it takes to capture the points" self clearPoints. self endTime: nil. self escapePossible: true. self startTime: Time millisecondClockValue. self currentDisplayProperties: self displayProperties. ! ! !CRRecognizer methodsFor: 'accessing' stamp: 'NS 7/26/2001 17:51'! stop "Stop capturing points. This stops the clock measuring how long it took to capture the points. It returns the informations about the coordinates of the stroke (in global screen coordinates (pixel))" self endTime ifNil: [self endTime: Time millisecondClockValue. self points last ~= self lastPoint ifTrue: [self addPointWithoutTest: self lastPoint]]. self echo ifNotNil: [self echo delete. self echo: nil]. ^ self coordinates start: self points first; end: self points last.! ! !CRRecognizer methodsFor: 'accessing' stamp: 'NS 8/9/2001 15:34'! stopAndCalculateFeature "Stop the recognizer and calculate the feature according to the captured points" | feature factor divisor beforeVector afterVector transPoints p size minSquaredLen minAngle index dirs dirPoints dirPointsSize | self stop. size _ self points size. "Special features" size = 0 ifTrue: [^ nil]. (size = 1 or: [(self absMaxCoord: self extent) < self currentDisplayProperties minMoveDistance]) ifTrue: [^ CRDotFeature new time: self time]. "Real stroke feature" dirs _ Array new: size. dirPoints _ Array new: size. transPoints _ PointArray new: size. "Calculate and store values to translate points from global screen coordinates (pixels) into normalized feature coordinates. Looks a little bit complicated but this is necessary to avoid floating point operations!!" factor _ self calcPointNormFactor. divisor _ self calcPointNormDivisor. beforeVector _ self topLeft. afterVector _ self calcPointNormShiftVector. minSquaredLen _ (CRFeature maxSize * self parameters minDirectionLengthPercentage // 100). minSquaredLen _ minSquaredLen * minSquaredLen. minAngle _ self parameters minAngle. "Iterate over the captured points" index _ 0. dirPointsSize _ 0. self points do: [:each | index _ index + 1. "p _ each - beforeVector * factor // divisor + afterVector." p _ each - beforeVector * factor. p _ (p x // divisor + afterVector x) @ (p y // divisor + afterVector y). transPoints at: index put: p. dirPointsSize _ self addDirectionTowardsPoint: p minSquaredLength: minSquaredLen minAngle: minAngle last: index = size dirPointsSize: dirPointsSize directions: dirs directionPoints: dirPoints]. "Ensure that no direction vector is longer than a certain maximum" dirPoints _ self ensureMaxDirectionLength: dirPoints size: dirPointsSize. "Generate stroke feature" feature _ CRStrokeFeature new. feature time: self time; capturedPoints: transPoints; directionVectors: (self directionVectors: dirPoints); extent: self calcNormExtent; startPoint: dirPoints first; points: dirPoints; calcAndSetSecondaryValues. ^ feature! ! !CRRecognizer class methodsFor: 'version' stamp: 'NS 8/8/2001 15:07'! checkPluginVersion "Use this method to check whether the Genie version in the image is compatible to the Genie VM plugin. The method returns a Boolean and writes some more information on Transcript. Note: Plugin and Image are compatible when they have the same major number" | classMajor pluginMajor msg p s pluginVersion | classMajor _ self majorNO. pluginMajor _ GeniePlugin majorNO. pluginMajor = classMajor ifFalse: [pluginMajor isNil ifTrue: [p _ 'The plugin is not available or too old for the current Genie version.'. s _ 'Update/install the plugin.'] ifFalse: [pluginMajor < classMajor ifTrue: [p _ 'The plugin is too old for the current Genie version.'. s _ 'Update the plugin.'] ifFalse: [p _ 'The current Genie version is too old for the plugin.'. s _ 'Update your image.']]]. pluginVersion _ pluginMajor isNil ifTrue: ['NA'] ifFalse: [GeniePlugin versionString]. msg _ p isNil ifTrue: ['GENIE: The plugin is compatible to the current Genie version'] ifFalse: ['GENIE: Version conflict!! Genie cannot use the VM-based algorithms and will be very slow!!']. msg _ msg, ' - Genie version: ', self versionString, ' - Plugin version: ', pluginVersion. p ifNotNil: [msg _ msg, ' - Problem: ', p, ' - Solution: ', s]. Transcript cr; show: msg; cr. ^ p isNil! ! !CRRecognizer class methodsFor: 'version' stamp: 'NS 8/8/2001 14:50'! majorNO ^ 2! ! !CRRecognizer class methodsFor: 'version' stamp: 'NS 8/15/2001 19:41'! minorNO ^ 20 ! ! !CRRecognizer class methodsFor: 'version' stamp: 'NS 8/8/2001 14:50'! versionNO ^ self majorNO * 1000 + self minorNO ! ! !CRRecognizer class methodsFor: 'version' stamp: 'NS 8/8/2001 15:07'! versionString ^ 'v', (self versionNO / 1000 asFloat) asString! ! !CRRecognizer class methodsFor: 'computation' stamp: 'NS 7/30/2001 12:03'! degreesX: x y: y "Answer the angle (in integer-degrees [0, 359]) of the vector x@y. This is an approximation with accuracy +/-1 degree" | tan theta big small negY absX absY | negY _ 0 - y. absX _ x < 0 ifTrue: [0 - x] ifFalse: [x]. absY _ y < 0 ifTrue: [negY] ifFalse: [y]. absX > absY ifTrue: [big _ x. small _ y] ifFalse: [big _ y. small _ x]. ^ big = 0 ifTrue: [0] ifFalse: ["Arctan approximation: arcTan x = x / (1 + (0.280872 * x * x))" tan _ small * 100 // big. theta _ tan * 572958 // (1000000 + (28 * tan * tan)). x > y ifTrue: [x >= negY ifTrue: [theta >= 0 ifTrue: [theta] ifFalse: [360 + theta]] ifFalse: [270 - theta]] ifFalse: [x >= negY ifTrue: [90 - theta] ifFalse: [180 + theta]]].! ! !CRStrokeFeature methodsFor: 'private' stamp: 'NS 8/15/2001 12:07'! calcAcuteAngleArray: angleCollection squaredLengths: squaredLenCollection "This method calculates several key-values describing the acute angles of the feature. In fact, it calculates the sum of the acute angles (weighted by heuristics) and the avarage position of the acute angles within the stroke. Acute ngle in this context mean angles, that are either very big or that don't have angles with the same sign in their neighbourhood" | acutePos totalLenSum maxAngleSum weightedAcuteCount lenCollection minAngle fullLen fullAngle angleSize lenSize signArray dirV | angleSize _ angleCollection size. lenSize _ squaredLenCollection size. minAngle _ 60. "Minumum angle to be possibly considered an acute angle" fullAngle _ 150. "If an angle is at least that big, it is 100% acute" maxAngleSum _ 30. "The angle in the neighbourhood of an acute angle are not allowed to be bigger than that." fullLen _ self class maxSize // 4. "The neighbourhood of an acute angle is searched for other angles within this distance" "lenCollection _ squaredLenCollection collect: [:each | each sqrt truncated]." dirV _ self directionVectors. lenCollection _ Array new: dirV size. 1 to: dirV size do: [:index | lenCollection at: index put: (self fastLen: (dirV at: index))]. weightedAcuteCount _ 0. acutePos _ 0. totalLenSum _ 0. signArray _ {1. -1}. "Iterate through all the angles an check wheter they are acute" angleCollection withIndexDo: [:angle :index | | absAngle angleSign | totalLenSum _ totalLenSum + (lenCollection at: index). "absAngle _ angle abs. angleSign _ angle sign." angle >= 0 ifTrue: [absAngle _ angle. angleSign _ angle = 0 ifTrue: [0] ifFalse: [1]] ifFalse: [absAngle _ 0 - angle. angleSign _ -1]. absAngle > minAngle ifTrue: [ | factor minFactor | minFactor _ 100. "Search the neighbourhood in both direction for other angled" signArray do: [:sign | | angleIndex lenIndex lenSum angleSum tempAngleSum | angleSum _ 0. tempAngleSum _ 0. lenSum _ 0. angleIndex _ index + sign. lenIndex _ sign = 1 ifTrue: [index + 1] ifFalse: [index]. [angleIndex >= 0 and: [angleIndex <= (angleSize + 1) and: [lenSum < fullLen and: [tempAngleSum < maxAngleSum]]]] whileTrue: [ | a | lenSum _ lenSum + (lenCollection at: lenIndex). (angleIndex >= 1 and: [angleIndex <= angleSize]) ifTrue: [a _ angleCollection at: angleIndex. a sign = angleSign ifTrue: [angleSum _ tempAngleSum. tempAngleSum _ tempAngleSum + a abs]]. angleIndex _ angleIndex + sign. lenIndex _ lenIndex + sign]. "Calculate the factor that describes how acute the current angle really is" factor _ maxAngleSum - angleSum. factor _ 100 * factor * factor // (maxAngleSum * maxAngleSum) * lenSum // fullLen * lenSum // fullLen. minFactor _ factor min: minFactor]. minFactor > 0 ifTrue: [ (index = 1 or: [index = angleSize]) ifTrue: [ | len | len _ index = 1 ifTrue: [squaredLenCollection at: 1] ifFalse: [squaredLenCollection at: lenSize]. absAngle _ self weightedStartEndAngle: absAngle squaredLength: len]. absAngle < fullAngle ifTrue: [minFactor _ minFactor * absAngle * absAngle // (fullAngle * fullAngle)]. "Update the total values" weightedAcuteCount _ weightedAcuteCount + minFactor. acutePos _ acutePos + (totalLenSum // 100 * minFactor)]]]. totalLenSum _ totalLenSum + lenCollection last. acutePos _ weightedAcuteCount = 0 ifTrue: [0] ifFalse: [acutePos // weightedAcuteCount * 10000 // totalLenSum]. ^ {weightedAcuteCount. acutePos}. ! ! !CRStrokeFeature methodsFor: 'private' stamp: 'NS 7/26/2001 15:48'! calcGlobalAngles | vSize result | vSize _ directionVectors size. result _ IntegerArray new: vSize. 1 to: vSize do: [:index | | v | v _ directionVectors at: index. result at: index put: (CRRecognizer degreesX: v x y: v y)]. ^ result! ! !CRStrokeFeature methodsFor: 'private' stamp: 'NS 5/28/2001 21:43'! calcPoints "Calculate the start/end points of the direction vectors" | collection | collection _ PointArray new: self directionVectors size + 1. collection at: 1 put: self originalStartPoint. self directionVectors withIndexDo: [:each :index | collection at: index + 1 put: (collection at: index) + each]. ^ collection! ! !CRStrokeFeature methodsFor: 'private' stamp: 'NS 7/17/2001 11:17'! calcPosAngleSum: angleCollection squaredLengths: lenCollection maxAngle: maxAngleNumber "Calculate the sum of the positive angles in angleCollection. The angles are only fuly counted if they are smaller than maxAngleNumber. Angles at the start or the end of the stroke are only fully counted if the start/end line is long enough. (There is often noise at the start or at the end)" | angleSum max vSize | angleSum _ 0. max _ maxAngleNumber isNil ifTrue: [SmallInteger maxVal - 100] ifFalse: [maxAngleNumber]. vSize _ angleCollection size. angleCollection withIndexDo: [:each :index | each > 0 ifTrue: [| a | a _ each > max ifTrue: [each * (180 - each) * (180 - each) // (180 - maxAngleNumber * (180 - maxAngleNumber))] ifFalse: [each]. a _ (index = 1 or: [index = vSize]) ifTrue: [| len | len _ index = 1 ifTrue: [lenCollection first] ifFalse: [lenCollection last]. self weightedStartEndAngle: a squaredLength: len] ifFalse: [a]. angleSum _ angleSum + a]]. ^ angleSum! ! !CRStrokeFeature methodsFor: 'private' stamp: 'NS 7/17/2001 11:23'! calcSize "The size of the feature" | x y | x _ self extent x. y _ self extent y. ^ x > y ifTrue: [x] ifFalse: [y]. ! ! !CRStrokeFeature methodsFor: 'private' stamp: 'NS 7/25/2001 12:39'! calcSquaredLengths | vSize result | vSize _ directionVectors size. result _ IntegerArray new: vSize. 1 to: vSize do: [:index | | v x y | v _ directionVectors at: index. x _ v x. y _ v y. result at: index put: (x * x + (y * y))]. ^ result ! ! !CRStrokeFeature methodsFor: 'private' stamp: 'NS 8/15/2001 12:12'! calcSquaredWeightedLengths "Calculate the sqared length and weight them by a heuristic functions. This function makes long lengths even longer. This is nice because eliminating long distances in a edit distance algorithm must be very expensive!!" | max dirV sl wl | max _ CRFeature maxSize. max _ max * max. "^ self squaredLengths collect: [:each | each * ((each sqrt truncated // 6) min: 40) // 10]." dirV _ self directionVectors. sl _ self squaredLengths. wl _ IntegerArray new: dirV size. 1 to: dirV size do: [:index | wl at: index put: (sl at: index) * (((self fastLen: (dirV at: index)) // 6) min: 40) // 10]. ^ wl! ! !CRStrokeFeature methodsFor: 'private' stamp: 'NS 5/30/2001 21:03'! directionVectors: aSequenceableCollection directionVectors _ aSequenceableCollection. directionVectors ifNotNil: [directionVectors _ directionVectors asPointArray].! ! !CRStrokeFeature methodsFor: 'private' stamp: 'NS 7/30/2001 15:59'! drawPoints: aSequenceableCollection on: aForm topLeft: aPoint scaleFactor: aNumberOrPoint | pen r g b colorStep | g _ 0. r _ 1. b _ 0. colorStep _ 1 / aSequenceableCollection size. pen _ Pen newOnForm: aForm. pen place: aPoint + (aSequenceableCollection first * aNumberOrPoint). pen color: (Color r: r g: g b: b). pen roundNib: 4. pen go: 1. pen roundNib: 2. aSequenceableCollection do: [:each | r _ r - colorStep. b _ b + colorStep. pen color: (Color r: r g: g b: b); goto: aPoint + (each * aNumberOrPoint)]. pen roundNib: 4. pen go: 1. ^ aForm! ! !CRStrokeFeature methodsFor: 'private' stamp: 'NS 8/15/2001 12:06'! fastLen: aPoint | x y min | x _ aPoint x. x < 0 ifTrue: [x _ 0 - x]. y _ aPoint y. y < 0 ifTrue: [y _ 0 - y]. min _ x > y ifTrue: [y] ifFalse: [x]. ^ x + y - (min * 2 // 3).! ! !CRStrokeFeature methodsFor: 'private' stamp: 'NS 5/31/2001 20:37'! globalAngles: aSequenceableCollection globalAngles _ aSequenceableCollection. globalAngles ifNotNil: [globalAngles _ globalAngles asIntegerArray].! ! !CRStrokeFeature methodsFor: 'private' stamp: 'NS 7/26/2001 16:54'! points: aSequenceableCollection relativePoints _ aSequenceableCollection. relativePoints ifNotNil: [relativePoints _ relativePoints asPointArray].! ! !CRStrokeFeature methodsFor: 'private' stamp: 'NS 7/25/2001 13:36'! size: anInteger sizeOrSquaredLengths _ anInteger! ! !CRStrokeFeature methodsFor: 'private' stamp: 'NS 5/31/2001 20:36'! squaredWeightedLengths: aSequenceableCollection squaredWeightedLengths _ aSequenceableCollection. squaredWeightedLengths ifNotNil: [squaredWeightedLengths _ squaredWeightedLengths asIntegerArray].! ! !CRStrokeFeature methodsFor: 'private' stamp: 'NS 5/28/2001 20:54'! substAngleFactorFrom: startDegreeNumber to: endDegreeNumber "This is a heuristic method. When a vector of one feature should be matched with a vector of another feature, the global angle difference is an important criteria. In the current implementation, the matching costs are first calculated independent of the gobal angle difference. But then, they are multiplied with this factor and then diveded by 16. (Actually, this factor is 16 times more than it should be to avoid floating point ops)." | absDiff | absDiff _ (endDegreeNumber - startDegreeNumber) abs. absDiff _ absDiff > 180 ifTrue: [360 - absDiff] ifFalse: [absDiff]. ^ absDiff * absDiff bitShift: -6! ! !CRStrokeFeature methodsFor: 'accessing' stamp: 'NS 7/25/2001 13:48'! calcAndSetSecondaryValues "Calculate the secondary values of the feature. If there is only one reference feature for a stroke, the secondary values are just calculated and store like this. But, if there are more reference features for one single character, the secondary values are set to the avarage." | acuteArray | "Use instance variable to temporarily store squaredLengths" sizeOrSquaredLengths _ self calcSquaredLengths. self fillCacheIfNotFull. self posAngleSum: self calcPosAngleSum. self negAngleSum: self calcNegAngleSum. self absAngleSum: self calcAbsAngleSum. acuteArray _ self calcAcuteAngleArray. self acuteAngleCount: acuteArray first. self acuteAnglePosition: acuteArray second. self endPoint: self calcEndPoint. self size: self calcSize. ! ! !CRStrokeFeature methodsFor: 'accessing' stamp: 'NS 5/30/2001 21:03'! capturedPoints: aSequenceableCollection capturedPoints _ aSequenceableCollection. capturedPoints ifNotNil: [capturedPoints _ capturedPoints asPointArray].! ! !CRStrokeFeature methodsFor: 'accessing' stamp: 'NS 7/11/2001 16:56'! fastGlobalAngles ^ globalAngles! ! !CRStrokeFeature methodsFor: 'accessing' stamp: 'NS 7/26/2001 16:52'! fastPoints ^ relativePoints! ! !CRStrokeFeature methodsFor: 'accessing' stamp: 'NS 7/11/2001 16:57'! fastSquaredWeightedLengths ^ squaredWeightedLengths! ! !CRStrokeFeature methodsFor: 'accessing' stamp: 'NS 7/26/2001 16:54'! fillCache "Cache often used values" self points: self calcPoints. self squaredWeightedLengths: self calcSquaredWeightedLengths. self globalAngles: self calcGlobalAngles.! ! !CRStrokeFeature methodsFor: 'accessing' stamp: 'NS 7/26/2001 16:56'! fillCacheIfNotFull "Cache often used values" relativePoints ifNil: [self points: self calcPoints]. squaredWeightedLengths ifNil: [self squaredWeightedLengths: self calcSquaredWeightedLengths. self globalAngles: self calcGlobalAngles]. ! ! !CRStrokeFeature methodsFor: 'accessing' stamp: 'NS 7/12/2001 10:27'! globalAngles self fillCacheIfNotFull. ^ globalAngles! ! !CRStrokeFeature methodsFor: 'accessing' stamp: 'NS 7/26/2001 17:03'! points self fillCacheIfNotFull. ^ relativePoints! ! !CRStrokeFeature methodsFor: 'accessing' stamp: 'NS 7/26/2001 16:54'! releaseCache "Delete the cache" self points: nil. self squaredWeightedLengths: nil. self globalAngles: nil.! ! !CRStrokeFeature methodsFor: 'accessing' stamp: 'NS 7/30/2001 15:40'! setSecondaryValuesToAvg: aCollection "Set the secondary values (the values that can be derived from others) to the avarage of the values of the features in the collection. If there is only one reference feature for a stroke, the secondary values are just derived. But, if there are more reference features for one single character, the secondary values are set to the avarage using this method" | count | count _ 0. aCollection do: [:each | each isStroke ifTrue: [count _ count + 1]]. count = 0 ifTrue: [^ self]. self resetSecondaryValues. aCollection do: [:each | each isStroke ifTrue: [self addSecondaryValues: each]]. self posAngleSum: self posAngleSum // count. self negAngleSum: self negAngleSum // count. self absAngleSum: self absAngleSum // count. self acuteAngleCount: self acuteAngleCount // count. self acuteAnglePosition: self acuteAnglePosition // count. self endPoint: self endPoint // count. self size: self calcSize. self isCacheFull ifTrue: [self fillCache].! ! !CRStrokeFeature methodsFor: 'accessing' stamp: 'NS 7/25/2001 13:35'! size ^ sizeOrSquaredLengths. ! ! !CRStrokeFeature methodsFor: 'accessing' stamp: 'NS 7/25/2001 13:48'! squaredLengths ^ (sizeOrSquaredLengths isNumber or: [sizeOrSquaredLengths isNil]) ifTrue: [self calcSquaredLengths] ifFalse: [sizeOrSquaredLengths] ! ! !CRStrokeFeature methodsFor: 'accessing' stamp: 'NS 7/12/2001 10:29'! squaredWeightedLengths self fillCacheIfNotFull. ^ squaredWeightedLengths! ! !CRStrokeFeature methodsFor: 'private comparing' stamp: 'NS 7/26/2001 16:53'! origSameClassAbsoluteStrokeDistance: aCRFeature forReference: aBoolean "Calculate a distance measuring the costs of matching all the direction vectors of the two features. The used algorithm is known as an algorithm to compute the edit distance between two strings or DNA-sequences. Basically it iterates through all the direction vectors of the two features and tries to make them identical. To achieve this goal, the algorithm can insert, replace or substitute direction vectors. Each of these 3 operations has costs assigned to it and the algorithm finds the sequence of them that generates the minimum costs. The alghorithm has a time usage of n * m (if n and m are the numbers of vectors in the two features). The main part of this implementation is not the basic algorithm, but th heuristics to calculate the costs for the operations. Besides the direction vectors, also the points where the vector starts / ends, the global angles of the vectors, the number of successive insert/remove operations without an intermediate substitution, etc. If aBoolean is true, the algorithm doesn't add special additional costs for successive insert/ remove ops. Further their is no substitution possible. This mode is used to calculate the distance to a reference feature that consists only of one point. Thus, this distance is an approximation of the costs that are used to build the feature from scratch. When th stroke-distance costs have to be normalized, this specieal reference costs are considered as the maximum distance NOTE: If this method is changed, the method updateStrokeDistance should perhaps also be changed" | myPoints otherPoints myVectors otherVectors mySquaredLengths otherSquaredLengths base rowInsertRemove rowBase rowInsertRemoveCount insertRemove myAngles otherAngles additionalMultiInsertRemoveCost maxDist maxSize myPAt1 otherPAt1 mySLAtIM1 myPAtIM1 otherPAtJM1 jLimit iLimit | "Maximum distance value stored in the matrix" maxDist _ 1 bitShift: 29. maxSize _ self class maxSize. "Additional costs if there are multiple succesive insert removes without an intermediate substitution" additionalMultiInsertRemoveCost _ aBoolean ifTrue: [0] ifFalse: [maxSize * maxSize bitShift: -10]. "Store the used arrays in local variables" self fillCacheIfNotFull. aCRFeature fillCacheIfNotFull. myPoints _ self fastPoints. otherPoints _ aCRFeature fastPoints. myVectors _ self directionVectors. otherVectors _ aCRFeature directionVectors. mySquaredLengths _ self fastSquaredWeightedLengths. otherSquaredLengths _ aCRFeature fastSquaredWeightedLengths. myAngles _ self fastGlobalAngles. otherAngles _ aCRFeature fastGlobalAngles. "rowBase stores the base values of the current/last row. rowInsertRemove stores the insert/remove costs. rowInsertRmoveCount stores the number of successive insert/remove operations." jLimit _ otherVectors size + 1. rowBase _ Array new: jLimit. rowInsertRemove _ Array new: jLimit. rowInsertRemoveCount _ Array new: jLimit. "Initialize row. First row is a special case" iLimit _ myVectors size + 1. rowBase at: 1 put: 0. rowInsertRemove at: 1 put: 0. rowInsertRemoveCount at: 1 put: 2. insertRemove _ additionalMultiInsertRemoveCost negated. "Common subexpressions" myPAt1 _ myPoints at: 1. otherPAt1 _ otherPoints at: 1. 2 to: jLimit do: [:j | | jM1 | "Common subexpressions" jM1 _ j - 1. insertRemove _ insertRemove + ((otherSquaredLengths at: jM1) + (self squaredDistanceFrom: (otherPoints at: jM1) to: myPAt1) bitShift: -7) + additionalMultiInsertRemoveCost. rowInsertRemove at: j put: insertRemove. rowBase at: j put: insertRemove * jM1. rowInsertRemoveCount at: j put: j]. "Nested iteration through all the vectors of both of the features" insertRemove _ (rowInsertRemove at: 1) - additionalMultiInsertRemoveCost. 2 to: iLimit do: [:i | | substBase iM1 | "Common subexpressions" iM1 _ i - 1. mySLAtIM1 _ mySquaredLengths at: iM1. myPAtIM1 _ myPoints at: iM1. "Subst base: Base cost for a possible substitution" substBase _ rowBase at: 1. insertRemove _ insertRemove + (mySLAtIM1 + (self squaredDistanceFrom: myPAtIM1 to: otherPAt1) bitShift: -7) + additionalMultiInsertRemoveCost. rowInsertRemove at: 1 put: insertRemove. rowBase at: 1 put: insertRemove * iM1. rowInsertRemoveCount at: 1 put: i. 2 to: jLimit do: [:j | | insert remove subst removeBase insertBase insertRemoveCount jM1 | "Common subexpressions" jM1 _ j - 1. otherPAtJM1 _ otherPoints at: jM1. "RemoveBase / insertBase: Base costs for possible removals / insertions" removeBase _ rowBase at: j. insertBase _ rowBase at: jM1. "Calculate the costs for a removal at the current position" remove _ mySLAtIM1 + (self squaredDistanceFrom: myPAtIM1 to: (otherPoints at: j)) bitShift: -7. (insertRemove _ rowInsertRemove at: j) = 0 ifTrue: [removeBase _ removeBase + remove] ifFalse: [removeBase _ removeBase + insertRemove + (remove * (rowInsertRemoveCount at: j)). remove _ remove + insertRemove]. "Calculate the costs for an insertion at the current position" insert _ (otherSquaredLengths at: jM1) + (self squaredDistanceFrom: otherPAtJM1 to: (myPoints at: i)) bitShift: -7. (insertRemove _ rowInsertRemove at: jM1) = 0 ifTrue: [insertBase _ insertBase + insert] ifFalse: [insertBase _ insertBase + insertRemove + (insert * (rowInsertRemoveCount at: jM1)). insert _ insert + insertRemove]. "Calculate the costs for a substitution at the current position" aBoolean ifTrue: [substBase _ maxDist] ifFalse: [subst _ ((self squaredDistanceFrom: (otherVectors at: jM1) to: (myVectors at: iM1)) + (self squaredDistanceFrom: otherPAtJM1 to: myPAtIM1)) * (16 + (self substAngleFactorFrom: (otherAngles at: jM1) to: (myAngles at: iM1))) bitShift: -11. substBase _ substBase + subst]. "Find best operation at the current position" ((substBase <= removeBase) and: [substBase <= insertBase]) ifTrue: [base _ substBase. insertRemove _ 0. insertRemoveCount _ 1] ifFalse: [(removeBase <= insertBase) ifTrue: [base _ removeBase. insertRemove _ remove + additionalMultiInsertRemoveCost. insertRemoveCount _ (rowInsertRemoveCount at: j) + 1] ifFalse: [base _ insertBase. insertRemove _ insert + additionalMultiInsertRemoveCost. insertRemoveCount _ (rowInsertRemoveCount at: jM1) + 1]]. "Update costs" substBase _ rowBase at: j. rowBase at: j put: (base < maxDist ifTrue: [base] ifFalse: [maxDist]). "Inlined min:" rowInsertRemove at: j put: (insertRemove < maxDist ifTrue: [insertRemove] ifFalse: [maxDist]). "Inlined min:" rowInsertRemoveCount at: j put: insertRemoveCount]. insertRemove _ rowInsertRemove at: 1]. ^ base! ! !CRStrokeFeature methodsFor: 'private comparing' stamp: 'NS 8/8/2001 15:17'! primSameClassAbsoluteStrokeDistance: aCRFeature forReference: aBoolean "Calculate a distance measuring the costs of matching all the direction vectors of the two features. The used algorithm is known as an algorithm to compute the edit distance between two strings or DNA-sequences. Basically it iterates through all the direction vectors of the two features and tries to make them identical. To achieve this goal, the algorithm can insert, replace or substitute direction vectors. Each of these 3 operations has costs assigned to it and the algorithm finds the sequence of them that generates the minimum costs. The alghorithm has a time usage of n * m (if n and m are the numbers of vectors in the two features). The main part of this implementation is not the basic algorithm, but the heuristics to calculate the costs for the operations. Besides the direction vectors, also the points where the vector starts / ends, the global angles of the vectors, the number of successive insert/remove operations without an intermediate substitution, etc. If aBoolean is true, the algorithm doesn't add special additional costs for successive insert/ remove ops. Further their is no substitution possible. This mode is used to calculate the distance to a reference feature that consists only of one point. Thus, this distance is an approximation of the costs that are used to build the feature from scratch. When th stroke-distance costs have to be normalized, this specieal reference costs are considered as the maximum distance NOTES: If this method is changed, the method updateStrokeDistance should perhaps also be changed. The GeniePlugin has to mirror the algorithm here!!" | myPoints otherPoints myVectors otherVectors mySquaredLengths otherSquaredLengths base rowInsertRemove rowBase rowInsertRemoveCount insertRemove myAngles otherAngles additionalMultiInsertRemoveCost maxDist maxSize myPAt1 otherPAt1 mySLAtIM1 myPAtIM1 otherPAtJM1 jLimit iLimit maxSizeAndRefFlag | "Maximum distance value stored in the matrix" maxDist _ 1 bitShift: 29. maxSize _ self class maxSize. "Additional costs if there are multiple succesive insert removes without an intermediate substitution" additionalMultiInsertRemoveCost _ aBoolean ifTrue: [0] ifFalse: [maxSize * maxSize bitShift: -10]. "Store the used arrays in local variables" self fillCacheIfNotFull. aCRFeature fillCacheIfNotFull. myPoints _ self fastPoints. otherPoints _ aCRFeature fastPoints. myVectors _ self directionVectors. otherVectors _ aCRFeature directionVectors. mySquaredLengths _ self fastSquaredWeightedLengths. otherSquaredLengths _ aCRFeature fastSquaredWeightedLengths. myAngles _ self fastGlobalAngles. otherAngles _ aCRFeature fastGlobalAngles. "These temporary arrays are used in later in this method. Since memory allocation is expensive on some PDAs, we use statically allocated arrays if they are large enough. Otherwise, we allocate new arrays. rowBase stores the base values of the current/last row. rowInsertRemove stores the insert/remove costs. rowInsertRmoveCount stores the number of successive insert/remove operations." jLimit _ otherVectors size + 1. jLimit > 30 ifTrue: [rowBase _ IntegerArray new: jLimit. rowInsertRemove _ IntegerArray new: jLimit. rowInsertRemoveCount _ IntegerArray new: jLimit] ifFalse: [rowBase _ self class rowBase. rowInsertRemove _ self class rowInsertRemove. rowInsertRemoveCount _ self class rowInsertRemoveCount]. "maxSizeAndRefFlag contains the maxium feature size (pixel) and also indicates whether the reference flag (boolean) is set. Therefore the maximum size is moved to the left and the reference flag is stored in the LSB. Note: This is necessary to avoid more than 12 primitive parameters" maxSizeAndRefFlag _ maxSize bitShift: 1. aBoolean ifTrue: [maxSizeAndRefFlag _ maxSizeAndRefFlag + 1]. base _ self callPrimSameClassAbsoluteStrokeDistanceMyPoints: myPoints otherPoints: otherPoints myVectors: myVectors otherVectors: otherVectors mySquaredLengths: mySquaredLengths otherSquaredLengths: otherSquaredLengths myAngles: myAngles otherAngles: otherAngles maxSizeAndReferenceFlag: maxSizeAndRefFlag rowBase: rowBase rowInsertRemove: rowInsertRemove rowInsertRemoveCount: rowInsertRemoveCount. base ifNotNil: [^ base]. "If calling the prim has failed use the ST code here..." "Initialize row. First row is a special case" iLimit _ myVectors size + 1. rowBase at: 1 put: 0. rowInsertRemove at: 1 put: 0. rowInsertRemoveCount at: 1 put: 2. insertRemove _ additionalMultiInsertRemoveCost negated. "Common subexpressions" myPAt1 _ myPoints at: 1. otherPAt1 _ otherPoints at: 1. 2 to: jLimit do: [:j | | jM1 | "Common subexpressions" jM1 _ j - 1. insertRemove _ insertRemove + ((otherSquaredLengths at: jM1) + (self squaredDistanceFrom: (otherPoints at: jM1) to: myPAt1) bitShift: -7) + additionalMultiInsertRemoveCost. rowInsertRemove at: j put: insertRemove. rowBase at: j put: insertRemove * jM1. rowInsertRemoveCount at: j put: j]. "Nested iteration through all the vectors of both of the features" insertRemove _ (rowInsertRemove at: 1) - additionalMultiInsertRemoveCost. 2 to: iLimit do: [:i | | substBase iM1 | "Common subexpressions" iM1 _ i - 1. mySLAtIM1 _ mySquaredLengths at: iM1. myPAtIM1 _ myPoints at: iM1. "Subst base: Base cost for a possible substitution" substBase _ rowBase at: 1. insertRemove _ insertRemove + (mySLAtIM1 + (self squaredDistanceFrom: myPAtIM1 to: otherPAt1) bitShift: -7) + additionalMultiInsertRemoveCost. rowInsertRemove at: 1 put: insertRemove. rowBase at: 1 put: insertRemove * iM1. rowInsertRemoveCount at: 1 put: i. 2 to: jLimit do: [:j | | insert remove subst removeBase insertBase insertRemoveCount jM1 | "Common subexpressions" jM1 _ j - 1. otherPAtJM1 _ otherPoints at: jM1. "RemoveBase / insertBase: Base costs for possible removals / insertions" removeBase _ rowBase at: j. insertBase _ rowBase at: jM1. "Calculate the costs for a removal at the current position" remove _ mySLAtIM1 + (self squaredDistanceFrom: myPAtIM1 to: (otherPoints at: j)) bitShift: -7. (insertRemove _ rowInsertRemove at: j) = 0 ifTrue: [removeBase _ removeBase + remove] ifFalse: [removeBase _ removeBase + insertRemove + (remove * (rowInsertRemoveCount at: j)). remove _ remove + insertRemove]. "Calculate the costs for an insertion at the current position" insert _ (otherSquaredLengths at: jM1) + (self squaredDistanceFrom: otherPAtJM1 to: (myPoints at: i)) bitShift: -7. (insertRemove _ rowInsertRemove at: jM1) = 0 ifTrue: [insertBase _ insertBase + insert] ifFalse: [insertBase _ insertBase + insertRemove + (insert * (rowInsertRemoveCount at: jM1)). insert _ insert + insertRemove]. "Calculate the costs for a substitution at the current position" aBoolean ifTrue: [substBase _ maxDist] ifFalse: [subst _ ((self squaredDistanceFrom: (otherVectors at: jM1) to: (myVectors at: iM1)) + (self squaredDistanceFrom: otherPAtJM1 to: myPAtIM1)) * (16 + (self substAngleFactorFrom: (otherAngles at: jM1) to: (myAngles at: iM1))) bitShift: -11. substBase _ substBase + subst]. "Find best operation at the current position" ((substBase <= removeBase) and: [substBase <= insertBase]) ifTrue: [base _ substBase. insertRemove _ 0. insertRemoveCount _ 1] ifFalse: [(removeBase <= insertBase) ifTrue: [base _ removeBase. insertRemove _ remove + additionalMultiInsertRemoveCost. insertRemoveCount _ (rowInsertRemoveCount at: j) + 1] ifFalse: [base _ insertBase. insertRemove _ insert + additionalMultiInsertRemoveCost. insertRemoveCount _ (rowInsertRemoveCount at: jM1) + 1]]. "Update costs" substBase _ rowBase at: j. rowBase at: j put: (base < maxDist ifTrue: [base] ifFalse: [maxDist]). "Inlined min:" rowInsertRemove at: j put: (insertRemove < maxDist ifTrue: [insertRemove] ifFalse: [maxDist]). "Inlined min:" rowInsertRemoveCount at: j put: insertRemoveCount]. insertRemove _ rowInsertRemove at: 1]. ^ base! ! !CRStrokeFeature methodsFor: 'private comparing' stamp: 'NS 8/8/2001 15:17'! sameClassAbsoluteStrokeDistance: aCRFeature forReference: aBoolean "Calculate a distance measuring the costs of matching all the direction vectors of the two features. The used algorithm is known as an algorithm to compute the edit distance between two strings or DNA-sequences. Basically it iterates through all the direction vectors of the two features and tries to make them identical. To achieve this goal, the algorithm can insert, replace or substitute direction vectors. Each of these 3 operations has costs assigned to it and the algorithm finds the sequence of them that generates the minimum costs. The alghorithm has a time usage of n * m (if n and m are the numbers of vectors in the two features). The main part of this implementation is not the basic algorithm, but the heuristics to calculate the costs for the operations. Besides the direction vectors, also the points where the vector starts / ends, the global angles of the vectors, the number of successive insert/remove operations without an intermediate substitution, etc. If aBoolean is true, the algorithm doesn't add special additional costs for successive insert/ remove ops. Further their is no substitution possible. This mode is used to calculate the distance to a reference feature that consists only of one point. Thus, this distance is an approximation of the costs that are used to build the feature from scratch. When th stroke-distance costs have to be normalized, this specieal reference costs are considered as the maximum distance NOTES: If this method is changed, the method updateStrokeDistance should perhaps also be changed. The GeniePlugin has to mirror the algorithm here!!" | myPoints otherPoints myVectors otherVectors mySquaredLengths otherSquaredLengths base rowInsertRemove rowBase rowInsertRemoveCount insertRemove myAngles otherAngles additionalMultiInsertRemoveCost maxDist maxSize myPAt1 otherPAt1 mySLAtIM1 myPAtIM1 otherPAtJM1 jLimit iLimit maxSizeAndRefFlag | "Maximum distance value stored in the matrix" maxDist _ 1 bitShift: 29. maxSize _ self class maxSize. "Additional costs if there are multiple succesive insert removes without an intermediate substitution" additionalMultiInsertRemoveCost _ aBoolean ifTrue: [0] ifFalse: [maxSize * maxSize bitShift: -10]. "Store the used arrays in local variables" self fillCacheIfNotFull. aCRFeature fillCacheIfNotFull. myPoints _ self fastPoints. otherPoints _ aCRFeature fastPoints. myVectors _ self directionVectors. otherVectors _ aCRFeature directionVectors. mySquaredLengths _ self fastSquaredWeightedLengths. otherSquaredLengths _ aCRFeature fastSquaredWeightedLengths. myAngles _ self fastGlobalAngles. otherAngles _ aCRFeature fastGlobalAngles. "These temporary arrays are used in later in this method. Since memory allocation is expensive on some PDAs, we use statically allocated arrays if they are large enough. Otherwise, we allocate new arrays. rowBase stores the base values of the current/last row. rowInsertRemove stores the insert/remove costs. rowInsertRmoveCount stores the number of successive insert/remove operations." jLimit _ otherVectors size + 1. jLimit > 30 ifTrue: [rowBase _ IntegerArray new: jLimit. rowInsertRemove _ IntegerArray new: jLimit. rowInsertRemoveCount _ IntegerArray new: jLimit] ifFalse: [rowBase _ self class rowBase. rowInsertRemove _ self class rowInsertRemove. rowInsertRemoveCount _ self class rowInsertRemoveCount]. "maxSizeAndRefFlag contains the maxium feature size (pixel) and also indicates whether the reference flag (boolean) is set. Therefore the maximum size is moved to the left and the reference flag is stored in the LSB. Note: This is necessary to avoid more than 12 primitive parameters" maxSizeAndRefFlag _ maxSize bitShift: 1. aBoolean ifTrue: [maxSizeAndRefFlag _ maxSizeAndRefFlag + 1]. base _ self callPrimSameClassAbsoluteStrokeDistanceMyPoints: myPoints otherPoints: otherPoints myVectors: myVectors otherVectors: otherVectors mySquaredLengths: mySquaredLengths otherSquaredLengths: otherSquaredLengths myAngles: myAngles otherAngles: otherAngles maxSizeAndReferenceFlag: maxSizeAndRefFlag rowBase: rowBase rowInsertRemove: rowInsertRemove rowInsertRemoveCount: rowInsertRemoveCount. base ifNotNil: [^ base]. "If calling the prim has failed use the ST code here..." "Initialize row. First row is a special case" iLimit _ myVectors size + 1. rowBase at: 1 put: 0. rowInsertRemove at: 1 put: 0. rowInsertRemoveCount at: 1 put: 2. insertRemove _ additionalMultiInsertRemoveCost negated. "Common subexpressions" myPAt1 _ myPoints at: 1. otherPAt1 _ otherPoints at: 1. 2 to: jLimit do: [:j | | jM1 | "Common subexpressions" jM1 _ j - 1. insertRemove _ insertRemove + ((otherSquaredLengths at: jM1) + (self squaredDistanceFrom: (otherPoints at: jM1) to: myPAt1) bitShift: -7) + additionalMultiInsertRemoveCost. rowInsertRemove at: j put: insertRemove. rowBase at: j put: insertRemove * jM1. rowInsertRemoveCount at: j put: j]. "Nested iteration through all the vectors of both of the features" insertRemove _ (rowInsertRemove at: 1) - additionalMultiInsertRemoveCost. 2 to: iLimit do: [:i | | substBase iM1 | "Common subexpressions" iM1 _ i - 1. mySLAtIM1 _ mySquaredLengths at: iM1. myPAtIM1 _ myPoints at: iM1. "Subst base: Base cost for a possible substitution" substBase _ rowBase at: 1. insertRemove _ insertRemove + (mySLAtIM1 + (self squaredDistanceFrom: myPAtIM1 to: otherPAt1) bitShift: -7) + additionalMultiInsertRemoveCost. rowInsertRemove at: 1 put: insertRemove. rowBase at: 1 put: insertRemove * iM1. rowInsertRemoveCount at: 1 put: i. 2 to: jLimit do: [:j | | insert remove subst removeBase insertBase insertRemoveCount jM1 | "Common subexpressions" jM1 _ j - 1. otherPAtJM1 _ otherPoints at: jM1. "RemoveBase / insertBase: Base costs for possible removals / insertions" removeBase _ rowBase at: j. insertBase _ rowBase at: jM1. "Calculate the costs for a removal at the current position" remove _ mySLAtIM1 + (self squaredDistanceFrom: myPAtIM1 to: (otherPoints at: j)) bitShift: -7. (insertRemove _ rowInsertRemove at: j) = 0 ifTrue: [removeBase _ removeBase + remove] ifFalse: [removeBase _ removeBase + insertRemove + (remove * (rowInsertRemoveCount at: j)). remove _ remove + insertRemove]. "Calculate the costs for an insertion at the current position" insert _ (otherSquaredLengths at: jM1) + (self squaredDistanceFrom: otherPAtJM1 to: (myPoints at: i)) bitShift: -7. (insertRemove _ rowInsertRemove at: jM1) = 0 ifTrue: [insertBase _ insertBase + insert] ifFalse: [insertBase _ insertBase + insertRemove + (insert * (rowInsertRemoveCount at: jM1)). insert _ insert + insertRemove]. "Calculate the costs for a substitution at the current position" aBoolean ifTrue: [substBase _ maxDist] ifFalse: [subst _ ((self squaredDistanceFrom: (otherVectors at: jM1) to: (myVectors at: iM1)) + (self squaredDistanceFrom: otherPAtJM1 to: myPAtIM1)) * (16 + (self substAngleFactorFrom: (otherAngles at: jM1) to: (myAngles at: iM1))) bitShift: -11. substBase _ substBase + subst]. "Find best operation at the current position" ((substBase <= removeBase) and: [substBase <= insertBase]) ifTrue: [base _ substBase. insertRemove _ 0. insertRemoveCount _ 1] ifFalse: [(removeBase <= insertBase) ifTrue: [base _ removeBase. insertRemove _ remove + additionalMultiInsertRemoveCost. insertRemoveCount _ (rowInsertRemoveCount at: j) + 1] ifFalse: [base _ insertBase. insertRemove _ insert + additionalMultiInsertRemoveCost. insertRemoveCount _ (rowInsertRemoveCount at: jM1) + 1]]. "Update costs" substBase _ rowBase at: j. rowBase at: j put: (base < maxDist ifTrue: [base] ifFalse: [maxDist]). "Inlined min:" rowInsertRemove at: j put: (insertRemove < maxDist ifTrue: [insertRemove] ifFalse: [maxDist]). "Inlined min:" rowInsertRemoveCount at: j put: insertRemoveCount]. insertRemove _ rowInsertRemove at: 1]. ^ base! ! !CRStrokeFeature methodsFor: 'private comparing' stamp: 'NS 7/13/2001 12:15'! sameClassAcuteAngleDistance: aCRFeature maxNormDist: maxNumber "This mainly heuristic method measures the distance between two features in terms of acute angles and their positions" | myVal otherVal factor diff minCount significantCount result | "The position is only considered if the acute angle count is at least this value (An acute angle count of 100 means that there is one completely acute angle)." minCount _ 50. myVal _ self acuteAngleCount. otherVal _ aCRFeature acuteAngleCount. significantCount _ myVal > otherVal ifTrue: [myVal] ifFalse: [otherVal]. "max" diff _ (myVal - otherVal). diff < 0 ifTrue: [diff _ 0 - diff]. "abs" factor _ 500 * diff * diff // 40000. "500 * diff * diff // (200 * 200)". significantCount > minCount ifTrue: [myVal _ self acuteAnglePosition. otherVal _ aCRFeature acuteAnglePosition. diff _ (myVal - otherVal). diff < 0 ifTrue: [diff _ 0 - diff]. "abs" factor _ factor + (500 * diff * diff // 1600)]. result _ maxNumber * factor // 1000. ^ maxNumber > result ifTrue: [result] ifFalse: [maxNumber]. "min"! ! !CRStrokeFeature methodsFor: 'private comparing' stamp: 'NS 7/13/2001 13:36'! sameClassAngleDistance: aCRFeature maxNormDist: maxNumber "This heuristic method measures the distance between two features in terms of angle sums" | myVal otherVal factor diff min additionalMinAngle temp | additionalMinAngle _ 75. "Negative angle sum" myVal _ self negAngleSum. otherVal _ aCRFeature negAngleSum. diff _ myVal - otherVal. diff < 0 ifTrue: [diff _ 0 - diff]. "abs" min _ myVal < otherVal ifTrue: [myVal] ifFalse: [otherVal]. "min" min _ min + additionalMinAngle. factor _ 250 * diff // min * diff // min. "Positive angle sum" myVal _ self posAngleSum. otherVal _ aCRFeature posAngleSum. diff _ myVal - otherVal. diff < 0 ifTrue: [diff _ 0 - diff]. "abs" min _ myVal < otherVal ifTrue: [myVal] ifFalse: [otherVal]. "min" min _ min + additionalMinAngle. factor _ factor + (250 * diff // min * diff // min). factor > 1000 ifTrue: [factor _ 1000]. "min" "Absolute angle sum" myVal _ self absAngleSum. otherVal _ aCRFeature absAngleSum. diff _ myVal - otherVal. diff < 0 ifTrue: [diff _ 0 - diff]. "abs" min _ myVal < otherVal ifTrue: [myVal] ifFalse: [otherVal]. "min" min _ min + additionalMinAngle. temp _ 500 * diff // min * diff // min. temp > 1000 ifTrue: [temp _ 1000]. factor _ factor + temp. temp _ maxNumber * factor // 2000. ^ maxNumber > temp ifTrue: [temp] ifFalse: [maxNumber].! ! !CRStrokeFeature methodsFor: 'private comparing' stamp: 'NS 7/17/2001 10:56'! sameClassShapeDistance: aCRFeature maxNormDist: maxNumber "This heuristic function measures the distance between two features in terms of their bounding box' shapes." | t1 t2 mult div | "Calculate the relative shape difference and multiply it by 10" t1 _ self extent x * aCRFeature extent y. t2 _ self extent y * aCRFeature extent x. t1 > t2 ifTrue: [mult _ t1 - t2. div _ t2] ifFalse: [mult _ t2 - t1. div _ t1]. div = 0 ifTrue: [^ mult = 0 ifTrue: [0] ifFalse: [maxNumber]]. mult _ 10 * mult // div. "If the ratio is smaller than 7, use a quadratic function to map into the normalized distance space. (7 is mapped to 4/10 of the maximum distance). Otherwise use a linear function. The total function is continuous and 20 (or more) is mapped to the maximum distance" ^ mult < 7 ifTrue: [4 * mult * mult * maxNumber // 490] ifFalse: [mult > 20 ifTrue: [mult _ 20]. 4 * maxNumber // 10 + (mult - 7 * 6 * maxNumber // 130)]! ! !CRStrokeFeature methodsFor: 'private comparing' stamp: 'NS 7/19/2001 10:07'! sameClassStartEndDistance: aCRFeature maxNormDist: maxNormDist "This is a heuristic method that measures the distance between two features in terms of their relative start/end points." | startDistance distance endDistance diffX diffY xDiv yDiv maxSize mySize otherSize temp | maxSize _ self class maxSize. mySize _ self size. otherSize _ aCRFeature size. "Raster the start and end points. If the aspect ratio is extreme (e.g. 5:1), the algorithm must be more sensitive to differences in the smaller direction" xDiv _ (100 * self extent x // mySize + 100) * maxSize. xDiv _ xDiv + ((100 * aCRFeature extent x // otherSize + 100) * maxSize) // 400. yDiv _ (100 * self extent y // mySize + 100) * maxSize. yDiv _ yDiv + ((100 * aCRFeature extent y // otherSize + 100) * maxSize) // 400. "Calculate the start and end distances" diffX _ (self startPoint x - aCRFeature startPoint x). diffX < 0 ifTrue: [diffX _ 0 - diffX]. "abs" diffX _ diffX * 12 // xDiv. diffY _ (self startPoint y - aCRFeature startPoint y). diffY < 0 ifTrue: [diffY _ 0 - diffY]. "abs" diffY _ diffY * 12 // yDiv. startDistance _ diffX * diffX + (diffY * diffY). diffX _ (self endPoint x - aCRFeature endPoint x). diffX < 0 ifTrue: [diffX _ 0 - diffX]. "abs" diffX _ diffX * 12 // xDiv. diffY _ (self endPoint y - aCRFeature endPoint y). diffY < 0 ifTrue: [diffY _ 0 - diffY]. "abs" diffY _ diffY * 12 // yDiv. endDistance _ diffX * diffX + (diffY * diffY). "Heuristics: When either start or endpoint distance is very bad, the result should be bad even if the other distance is quite well" temp _ startDistance > endDistance ifTrue: [startDistance] ifFalse: [endDistance]. "max" distance _ (3 * startDistance) + (3 * endDistance) + (2 * (temp)) // 3. temp _ distance * maxNormDist // 144. ^ temp < maxNormDist ifTrue: [temp] ifFalse: [maxNormDist]. "min"! ! !CRStrokeFeature methodsFor: 'private comparing' stamp: 'NS 7/17/2001 17:15'! sameClassStrokeDistance: aCRFeature maxDist: maxNumber maxNormDist: maxNormNumber "Calculate the stroke-distance and normalize by a linear function mapping maxNumber to the maximum distance" | temp | temp _ (10 * (self sameClassAbsoluteStrokeDistance: aCRFeature forReference: false)) // (10 * maxNumber // maxNormNumber). ^ temp > maxNormNumber ifTrue: [maxNormNumber] ifFalse: [temp]! ! !CRStrokeFeature methodsFor: 'private comparing' stamp: 'NS 7/17/2001 17:16'! sameClassStrokeDistance: aCRFeature maxNormDist: maxNormNumber "Calculate an asymmetric distance measuring the costs of matching all the direction vectors of the two features. Basically this matching operation is completely symmetric. But the result of the operation is just a number that must be normalized somehow. (At the end the distance has to be in the interval [0, CRFeature maxNormDistance]). The problem is that the not yet normalized distance value can vary a lot depending on the number of direction vectors in the two features. That's the reason why the normalization process can't be done independent of the two currently compared features!! Usually one feature is compared to all the features in a dictionary. To be fair, the normalization process has to be the same for one dictionary lookup and thus this process can only use properties of the feature that is looked up. As a consequence, this distance is not symmetric ((A dist: B) ~= (B dist: A)). Whenever a new feature is looked up in a dictionary, the asymmetric stroke distance is used. For all the distances between features inside the dictionary, the symmetric stroke distance is used. (It would be really confusing if the distance from 'a' to 'b' would be different than the one from 'b' to 'a'!!)" ^ self sameClassStrokeDistance: aCRFeature maxDist: self maxStrokeDistance maxNormDist: maxNormNumber! ! !CRStrokeFeature methodsFor: 'private comparing' stamp: 'NS 7/17/2001 17:13'! sameClassSymmetricStrokeDistance: aCRFeature maxNormDist: maxNumber "Calculate a symmetric distance measuring the costs of matching all the direction vectors of the two features. Basically this matching operation is completely symmetric. But the result of the operation is just a number that must be normalized somehow. (At the end the distance has to be in the interval [0, CRFeature maxNormDistance]). The problem is that the not yet normalized distance value can vary a lot depending on the number of direction vectors in the two features. That's the reason why the normalization process can't be done independent of the two currently compared features!! Usually one feature is compared to all the features in a dictionary. To be fair, the normalization process has to be the same for one dictionary lookup and thus this process can only use properties of the feature that is looked up. As a consequence, this distance is not symmetric ((A dist: B) ~= (B dist: A)). Whenever a new feature is looked up in a dictionary, the asymmetric stroke distance is used. For all the distances between features inside the dictionary, the symmetric stroke distance is used. (It would be really confusing if the distance from 'a' to 'b' would be different than the one from 'b' to 'a'!!)" | m1 m2 | m1 _ self maxStrokeDistance. m2 _ aCRFeature maxStrokeDistance. m1 > m2 ifTrue: [m1 _ m2]. " min" ^ self sameClassStrokeDistance: aCRFeature maxDist: m1 maxNormDist: maxNumber! ! !CRStrokeFeature methodsFor: 'update' stamp: 'NS 7/25/2001 17:51'! updateToVersion2000 "Update the feature to version 2. Replace Array with IntegerArray resp. PointArray wherever it is possible. Update max. stroke distance" capturedPoints ifNotNil: [capturedPoints _ capturedPoints asPointArray]. directionVectors ifNotNil: [directionVectors _ directionVectors asPointArray]. self isCacheFull ifTrue: [self fillCache] ifFalse: [self releaseCache].! ! !CRStrokeFeature methodsFor: 'plugin control' stamp: 'NS 7/19/2001 16:28'! callPrimSameClassAbsoluteStrokeDistanceMyPoints: myPoints otherPoints: otherPoints myVectors: myVectors otherVectors: otherVectors mySquaredLengths: mySquaredLengths otherSquaredLengths: otherSquaredLengths myAngles: myAngles otherAngles: otherAngles maxSizeAndReferenceFlag: maxSizeAndRefFlag rowBase: rowBase rowInsertRemove: rowInsertRemove rowInsertRemoveCount: rowInsertRemoveCount "returns base, if OK; nil, if primitive failed" ^ nil! ! !CRStrokeFeature methodsFor: 'objects from disk' stamp: 'NS 7/25/2001 17:11'! readDataFrom: aDataStream size: varsOnDisk "The variable sizeOrSquaredLengths was added later and it might therefore not be available in saved dictioneries." | noSize | noSize _ false. ^ (varsOnDisk = self class instSize or: [noSize _ true. (varsOnDisk = (self class instSize - 1)) and: [self class allInstVarNames last = 'sizeOrSquaredLengths']]) ifTrue: [super readDataFrom: aDataStream size: varsOnDisk. noSize ifTrue: [self size: self calcSize]. self updateToVersion2000. self] ifFalse: [self error: 'Incompatible CRStrokeFeature format'].! ! !CRStrokeFeature methodsFor: 'objects from disk' stamp: 'NS 7/25/2001 15:00'! storeDataOn: aDataStream "This is basically the implementation in Object. For backwards compatibility, IntegerArrays and PointArrays are stored as Arrays and the instance variable sizeOrSquaredLengths (last variable!!!!) is not stored" | cntInstVars cntIndexedVars | cntInstVars _ self class instSize - 1. cntIndexedVars _ self basicSize. aDataStream beginInstance: self class size: cntInstVars + cntIndexedVars. self class allInstVarNames last = 'sizeOrSquaredLengths' ifFalse: [^ self error: 'Incompatible CRStrokeFeature format']. 1 to: cntInstVars do: [:i | | var | var _ self instVarAt: i. ((var isKindOf: IntegerArray) or: [var isKindOf: PointArray]) ifTrue: [ | newVar varCount | varCount _ var size. newVar _ Array new: varCount. 1 to: varCount do: [:index | newVar at: index put: (var at: index)]. var _ newVar]. aDataStream nextPut: var]. (aDataStream byteStream class == DummyStream) ifFalse: [ 1 to: cntIndexedVars do: [:i | aDataStream nextPut: (self basicAt: i)]]. "save a little time if dummy stream"! ! !CRStrokeFeature class methodsFor: 'accessing' stamp: 'NS 7/19/2001 16:36'! createTempArrays: sizeInteger RowBase _ IntegerArray new: sizeInteger. RowInsertRemove _ IntegerArray new: sizeInteger. RowInsertRemoveCount _ IntegerArray new: sizeInteger.! ! !CRStrokeFeature class methodsFor: 'accessing' stamp: 'NS 7/19/2001 16:34'! rowBase ^ RowBase! ! !CRStrokeFeature class methodsFor: 'accessing' stamp: 'NS 7/19/2001 16:34'! rowInsertRemove ^ RowInsertRemove! ! !CRStrokeFeature class methodsFor: 'accessing' stamp: 'NS 7/19/2001 16:34'! rowInsertRemoveCount ^ RowInsertRemoveCount! ! !CRStrokeFeature class methodsFor: 'class initialization' stamp: 'NS 7/19/2001 16:37'! initialize " CRStrokeFeature initialize " "Build a feature that consists only of one point. Therefore the absolute storke-distance between this reference feature and another feature are approximately the costs to build the other feature from scratch. This value is then used to normalize the absolute stroke-distance." StrokeReferenceFeature1 _ self new startPoint: 0 @ 0; directionVectors: (PointArray with: 0 @ 1); time: 0; extent: 0 @ 1; calcAndSetSecondaryValues. self createTempArrays: 30.! ! !CRStrokeFeature class methodsFor: 'primitive test' stamp: 'NS 7/12/2001 16:30'! testOriginalPerformance " CRStrokeFeature testOriginalPerformance " ^ self testPerformancePrimitive: false ! ! !CRStrokeFeature class methodsFor: 'primitive test' stamp: 'NS 7/12/2001 16:31'! testPerformanceBlock: block dictionary: aCRDictionary iterations: anInteger output: aBoolean | strokeFeatures time | strokeFeatures _ (aCRDictionary dictionary keys select: [:each | each isKindOf: self]) asArray. aBoolean ifTrue: [Transcript cr; show: 'Test dictionary contains ' , strokeFeatures size asString , ' stroke features, and we do ' , anInteger asString , ' iterations...'; cr]. time _ [anInteger timesRepeat: [strokeFeatures do: [:feature1 | strokeFeatures do: [:feature2 | block value: feature1 value: feature2]]]] timeToRun. aBoolean ifTrue: [Transcript show: 'Test finished. time: ' , time printString; cr]. ^ time! ! !CRStrokeFeature class methodsFor: 'primitive test' stamp: 'NS 7/19/2001 15:38'! testPerformanceMetrics: aSymbolOrNumber dictionaryName: dictSymbol iterations: anInteger output: outBoolean "CRStrokeFeature primitiveTestPerformanceDictionaryName: #PrimTest1" | forReference dict block maxNormDist | "Use the CRStrokeFeatures in this dictionary as test cases" dict _ CRDictionary name: dictSymbol. dict isNil ifTrue: [self error: 'Dictionary ', dictSymbol, ' is not available!!']. dict fillFeaturesCacheIfNotFull. forReference _ false. maxNormDist _ self maxNormDistance. (aSymbolOrNumber = #absoluteStroke or: [aSymbolOrNumber = 1]) ifTrue: [block _ [:feature1 :feature2 | feature1 sameClassAbsoluteStrokeDistance: feature2 forReference: forReference]]. (aSymbolOrNumber = #primAbsoluteStroke or: [aSymbolOrNumber = 2]) ifTrue: [block _ [:feature1 :feature2 | feature1 primSameClassAbsoluteStrokeDistance: feature2 forReference: forReference]]. (aSymbolOrNumber = #origAbsoluteStroke or: [aSymbolOrNumber = 3]) ifTrue: [block _ [:feature1 :feature2 | feature1 origSameClassAbsoluteStrokeDistance: feature2 forReference: forReference]]. (aSymbolOrNumber = #stroke or: [aSymbolOrNumber = 4]) ifTrue: [block _ [:feature1 :feature2 | feature1 sameClassStrokeDistance: feature2 maxNormDist: maxNormDist]]. (aSymbolOrNumber = #symmetricStroke or: [aSymbolOrNumber = 5]) ifTrue: [block _ [:feature1 :feature2 | feature1 sameClassSymmetricStrokeDistance: feature2 maxNormDist: maxNormDist]]. (aSymbolOrNumber = #acuteAngle or: [aSymbolOrNumber = 6]) ifTrue: [block _ [:feature1 :feature2 | feature1 sameClassAcuteAngleDistance: feature2 maxNormDist: maxNormDist]]. (aSymbolOrNumber = #angle or: [aSymbolOrNumber = 7]) ifTrue: [block _ [:feature1 :feature2 | feature1 sameClassAngleDistance: feature2 maxNormDist: maxNormDist]]. (aSymbolOrNumber = #shape or: [aSymbolOrNumber = 8]) ifTrue: [block _ [:feature1 :feature2 | feature1 sameClassShapeDistance: feature2 maxNormDist: maxNormDist]]. (aSymbolOrNumber = #startEnd or: [aSymbolOrNumber = 9]) ifTrue: [block _ [:feature1 :feature2 | feature1 sameClassStartEndDistance: feature2 maxNormDist: maxNormDist]]. (aSymbolOrNumber = #size or: [aSymbolOrNumber = 10]) ifTrue: [block _ [:feature1 :feature2 | feature1 sizeDistance: feature2 maxNormDist: maxNormDist]]. (aSymbolOrNumber = #time or: [aSymbolOrNumber = 11]) ifTrue: [block _ [:feature1 :feature2 | feature1 timeDistance: feature2 maxNormDist: maxNormDist]]. ^ self testPerformanceBlock:block dictionary: dict iterations: anInteger output: outBoolean.! ! !CRStrokeFeature class methodsFor: 'primitive test' stamp: 'NS 7/13/2001 15:31'! testPerformanceMetrics: aSymbolOrNumber iterations: aNumber output: outBoolean ^ self testPerformanceMetrics: aSymbolOrNumber dictionaryName: #GenieTest iterations: aNumber output: outBoolean. ! ! !CRStrokeFeature class methodsFor: 'primitive test' stamp: 'NS 7/25/2001 16:43'! testPerformancePrimitive: aBoolean ^ self testPerformancePrimitive: aBoolean iterations: 1 output: true.! ! !CRStrokeFeature class methodsFor: 'primitive test' stamp: 'NS 7/13/2001 15:31'! testPerformancePrimitive: primBoolean iterations: aNumber output: outBoolean | metrics | metrics _ primBoolean ifTrue: [#primAbsoluteStroke] ifFalse: [#origAbsoluteStroke]. ^ self testPerformanceMetrics: metrics iterations: aNumber output: outBoolean. ! ! !CRStrokeFeature class methodsFor: 'primitive test' stamp: 'NS 7/12/2001 16:12'! testPrimitive " CRStrokeFeature testPrimitive " ^ self testPrimitiveWithDetails: false.! ! !CRStrokeFeature class methodsFor: 'primitive test' stamp: 'NS 7/12/2001 15:35'! testPrimitive: primBlock original: origBlock dictionary: aCRDictionary relDiffLimit: aNumber detailed: aBoolean "Arguments: primBlock: Block that takes two CRStrokeFeatures as arguments and compares them using the new primitive method. origBlock: Block that takes two CRStrokeFeatures as arguments and compares them using the original method. aCRDictionary: CRDictionary containing CRFeatures that should be used in the comparing methods. aNumber: The algorithm raises an exception if the relative difference between the original and the primitive method are bigger than that. Transcript output has the form: (index of feature1) - (index of feature2): (primitive), (original), (abs diff), (rel diff)" | strokeFeatures prim orig absDiff relDiff relDiffMax | relDiffMax _ 0. strokeFeatures _ (aCRDictionary dictionary keys select: [:each | each isKindOf: self]) asArray. Transcript cr; show: 'CRStrokeFeature test -- Relative difference limit is ', aNumber asString; cr. Transcript show: 'Test dictionary contains ', strokeFeatures size asString, ' stroke features'; cr. strokeFeatures withIndexDo: [:feature1 :index1 | strokeFeatures withIndexDo: [:feature2 :index2 | prim _ primBlock value: feature1 value: feature2. orig _ origBlock value: feature1 value: feature2. absDiff _ (orig - prim) abs. relDiff _ orig > 0 ifTrue: [absDiff / orig] ifFalse: [absDiff]. aBoolean ifTrue: [Transcript show: index1 asString, ' - ', index2 asString, ': ', prim asString, ', ', orig asString, ', ', absDiff asString, ', ', relDiff asString; cr]. relDiff > aNumber ifTrue: [self error: 'Relative difference larger than limit']. relDiffMax _ relDiffMax max: relDiff]]. Transcript show: 'Tests finished. Relative difference maximum: ', relDiffMax asString; cr.! ! !CRStrokeFeature class methodsFor: 'primitive test' stamp: 'NS 7/17/2001 18:09'! testPrimitiveDictionaryName: aDictSymbol forReference: forReferenceBoolean detailed: detailedBoolean "CRStrokeFeature primitiveTestDictionaryName: #PrimTest1 forReference: true. " "Method to test a primitive for CRStrokeFeature>>sameClassAbsoluteStrokeDistance:forReference:. See comment of method CRStrokeFeature class>>testPrimitive:original:dictionary:relDiffLimit:" | primSelector origSelector dict relDiffLimit | "Selectors for the original and the primitive methods" primSelector _ #primSameClassAbsoluteStrokeDistance:forReference:. origSelector _ #origSameClassAbsoluteStrokeDistance:forReference:. "This boolean value is used as the second argument of the test method. The test should run successfully with both true and false". Transcript cr; show: 'Testing primitive with dictionary: ', aDictSymbol, ', for reference: ', forReferenceBoolean printString, '...'. "Use the CRStrokeFeatures in this dictionary as test cases" "Note: You can use the method #exportedName: instead of #name: to refer to the exported name of a dictionary" dict _ CRDictionary name: aDictSymbol. dict isNil ifTrue: [self error: 'Dictionary ', aDictSymbol, ' is not available!!']. "If the relative difference between primitive and original is bigger than this value, an exception is raised" relDiffLimit _ 0. dict fillFeaturesCacheIfNotFull. self testPrimitive: [:feature1 :feature2 | feature1 perform: primSelector with: feature2 with: forReferenceBoolean] original: [:feature1 :feature2 | feature1 perform: origSelector with: feature2 with: forReferenceBoolean] dictionary: dict relDiffLimit: relDiffLimit detailed: detailedBoolean ! ! !CRStrokeFeature class methodsFor: 'primitive test' stamp: 'NS 7/12/2001 16:30'! testPrimitivePerformance " CRStrokeFeature testPrimitivePerformance " ^ self testPerformancePrimitive: true ! ! !CRStrokeFeature class methodsFor: 'primitive test' stamp: 'NS 7/13/2001 15:31'! testPrimitiveWithDetails: aBoolean CRStrokeFeature testPrimitiveDictionaryName: #GenieTest forReference: true detailed: aBoolean. CRStrokeFeature testPrimitiveDictionaryName: #GenieTest forReference: false detailed: aBoolean.! ! !CRStrokeFeature class methodsFor: 'update' stamp: 'NS 7/25/2001 17:52'! updateToVersion2000 "Updates all older StrokeFeatures to version 2000" self allSubInstancesDo: [:each | each updateToVersion2000. each extent ifNotNil: [each size: each calcSize]].! ! !CRStrokeMorph methodsFor: 'adding' stamp: 'NS 7/26/2001 11:22'! addPoint: aPoint "We can save some time if we don't open this morph before we know the first point." | r | (points size > 0 and: [points last = aPoint]) ifTrue: [^self]. points add: aPoint. r := points last extent: 1@1. bounds := points size = 1 ifTrue: [r] ifFalse: [bounds merge: r]. fullBounds := nil. points size = 1 ifTrue: [self openInWorld] ifFalse: [World canvas line: (points at: (points size-1 max: 1)) to: points last color: self color].! ! !FillInTheBlankMorph methodsFor: 'initialization' stamp: 'NS 7/9/2001 11:16'! setQuery: queryString initialAnswer: initialAnswer answerExtent: answerExtent acceptOnCR: acceptBoolean | query frame topOffset accept cancel buttonAreaHeight | response _ initialAnswer. done _ false. self removeAllMorphs. self layoutPolicy: ProportionalLayout new. query _ TextMorph new contents: queryString. query setNameTo: 'query'. query lock. frame _ LayoutFrame new. frame topFraction: 0.0; topOffset: 2. frame leftFraction: 0.5; leftOffset: (query width // 2) negated. query layoutFrame: frame. self addMorph: query. topOffset _ query height + 4. accept _ SimpleButtonMorph new target: self; color: Color veryLightGray. accept label: 'Accept(s)'; actionSelector: #accept. accept setNameTo: 'accept'. frame _ LayoutFrame new. frame rightFraction: 0.5; rightOffset: -10; bottomFraction: 1.0; bottomOffset: -2. accept layoutFrame: frame. self addMorph: accept. cancel _ SimpleButtonMorph new target: self; color: Color veryLightGray. cancel label: 'Cancel(l)'; actionSelector: #cancel. cancel setNameTo: 'cancel'. frame _ LayoutFrame new. frame leftFraction: 0.5; leftOffset: 10; bottomFraction: 1.0; bottomOffset: -2. cancel layoutFrame: frame. self addMorph: cancel. buttonAreaHeight _ (accept height max: cancel height) + 4. textPane _ PluggableTextMorph on: self text: #response accept: #response: readSelection: #selectionInterval menu: #codePaneMenu:shifted:. textPane extent: answerExtent. textPane hResizing: #spaceFill; vResizing: #spaceFill. textPane borderWidth: 2. textPane hasUnacceptedEdits: true. textPane acceptOnCR: acceptBoolean. textPane setNameTo: 'textPane'. frame _ LayoutFrame new. frame leftFraction: 0.0; rightFraction: 1.0; topFraction: 0.0; topOffset: topOffset; bottomFraction: 1.0; bottomOffset: buttonAreaHeight negated. textPane layoutFrame: frame. self addMorph: textPane. self extent: (query extent x max: answerExtent x) + 4 @ (topOffset + answerExtent y + 4 + buttonAreaHeight). ! ! !FillInTheBlankMorph class methodsFor: 'default constants' stamp: 'NS 7/9/2001 11:19'! defaultAnswerExtent ^ 200@60.! ! !FillInTheBlankMorphWithDictMenu methodsFor: 'code pane menu' stamp: 'NS 7/30/2001 16:55'! codePaneMenu: aMenu shifted: shifted CRDictionary addNamesToMenu: aMenu target: self selector: #dictionaryName:. ^ aMenu! ! !FillInTheBlankMorphWithDictMenu methodsFor: 'code pane menu' stamp: 'NS 7/6/2001 11:29'! originalCodePaneMenu: aMenu shifted: shifted ^ super codePaneMenu: aMenu shifted: shifted! ! !FillInTheBlankMorphWithDictMenu methodsFor: 'private' stamp: 'NS 7/30/2001 16:48'! dictionaryName: aString self simulateString: '''', aString, ''''.! ! !FillInTheBlankMorphWithDictMenu methodsFor: 'private' stamp: 'NS 7/5/2001 15:13'! simulateString: aString aString do: [:each | textPane keyStroke: (KeyboardEvent new setType: #keystroke buttons: 0 position: World currentHand position keyValue: each asciiValue hand: World currentHand stamp: Time millisecondClockValue)].! ! !FillInTheBlankMorphWithDictMenu methodsFor: 'initialization' stamp: 'NS 7/5/2001 15:47'! initialize super initialize. self setBalloonText: 'Use the context menu to insert dictionaries'! ! !FillInTheBlankMorphWithCharMenu methodsFor: 'code pane menu' stamp: 'NS 7/6/2001 11:28'! codePaneMenu: aMenu shifted: shifted | menu subMenu | menu _ self originalCodePaneMenu: aMenu shifted: shifted. menu addMorphFront: MenuLineMorph new. subMenu _ MenuItemMorph new contents: 'Genie'; arguments: #(); subMenu: self createGenieMenu. menu addMorphFront: subMenu. subMenu _ MenuItemMorph new contents: 'Text editing'; arguments: #(); subMenu: self createTextMenu. menu addMorphFront: subMenu. subMenu _ MenuItemMorph new contents: 'Mouse & Halos'; arguments: #(); subMenu: self createMouseMenu. menu addMorphFront: subMenu. subMenu _ MenuItemMorph new contents: 'Command keys'; arguments: #(); subMenu: self createCommandKeysMenu. menu addMorphFront: subMenu. subMenu _ MenuItemMorph new contents: 'Modifier keys'; arguments: #(); subMenu: self createModifierKeysMenu. menu addMorphFront: subMenu. subMenu _ MenuItemMorph new contents: 'Special chars'; arguments: #(); subMenu: self createSpecialCharsMenu. menu addMorphFront: subMenu. subMenu _ MenuItemMorph new contents: 'Digits'; arguments: #(); subMenu: self createDigitsMenu. menu addMorphFront: subMenu. subMenu _ MenuItemMorph new contents: 'Capital letters'; arguments: #(); subMenu: self createCapitalLettersMenu. menu addMorphFront: subMenu. subMenu _ MenuItemMorph new contents: 'Small letters'; arguments: #(); subMenu: self createSmallLettersMenu. menu addMorphFront: subMenu. ^ menu ! ! !FillInTheBlankMorphWithCharMenu methodsFor: 'code pane menu' stamp: 'NS 7/6/2001 11:26'! createSpecialCharsMenu | menu chars | menu _ self createBasicMenu. chars _ { $~. $`. $!!. $@. $#. $$. $%. $^. $&. $*. $(. $). $_. $-. $+. $=. ${. $}. $[. $]. $|. $\. $:. $;. $". $'. $<. $>. $,. $.. $?. $/}. menu add: 'Space' selector: #simulateString: argument: ' '. chars do: [:each | menu add: each asString selector: #simulateString: argument: each asString]. ^ menu! ! !FillInTheBlankMorphWithDictMenu class methodsFor: 'default constants' stamp: 'NS 7/5/2001 15:46'! defaultAnswerExtent ^ 220@120! ! !FillInTheBlankMorphWithDictMenu class methodsFor: 'instance creation' stamp: 'NS 7/5/2001 14:26'! request: queryString initialAnswer: defaultAnswer centerAt: aPoint inWorld: aWorld onCancelReturn: returnOnCancel ^ self request: queryString initialAnswer: defaultAnswer centerAt: aPoint inWorld: aWorld onCancelReturn: returnOnCancel acceptOnCR: false! ! !InterpreterPlugin methodsFor: 'debugging' stamp: 'sr 6/6/2001 23:47'! halt self cCode: '' inSmalltalk: [self halt].! ! !InterpreterPlugin methodsFor: 'debugging' stamp: 'sr 6/6/2001 23:46'! msg: s self var: #s declareC: 'char *s'. self cCode: 'fprintf(stderr, "\n%s: %s", moduleName, s)' inSmalltalk: [Transcript cr; show: self class moduleName , ': ' , s; endEntry]! ! !GeniePlugin methodsFor: 'computation' stamp: 'sr 6/5/2001 09:17'! cSquaredDistanceFrom: aPoint to: bPoint "arguments are pointer to ints paired as x,y coordinates of points" | aPointX aPointY bPointX bPointY xDiff yDiff | self var: #aPoint declareC: 'int * aPoint'. self var: #bPoint declareC: 'int * bPoint'. aPointX _ aPoint at: 0. aPointY _ aPoint at: 1. bPointX _ bPoint at: 0. bPointY _ bPoint at: 1. xDiff _ bPointX - aPointX. yDiff _ bPointY - aPointY. ^ xDiff * xDiff + (yDiff * yDiff)! ! !GeniePlugin methodsFor: 'computation' stamp: 'sr 6/5/2001 06:53'! cSubstAngleFactorFrom: startDegreeNumber to: endDegreeNumber | absDiff | absDiff _ (endDegreeNumber - startDegreeNumber) abs. absDiff > 180 ifTrue: [absDiff _ 360 - absDiff]. ^ absDiff * absDiff bitShift: -6! ! !GeniePlugin methodsFor: 'computation' stamp: 'NS 7/19/2001 17:57'! primSameClassAbsoluteStrokeDistanceMyPoints: myPointsOop otherPoints: otherPointsOop myVectors: myVectorsOop otherVectors: otherVectorsOop mySquaredLengths: mySquaredLengthsOop otherSquaredLengths: otherSquaredLengthsOop myAngles: myAnglesOop otherAngles: otherAnglesOop maxSizeAndReferenceFlag: maxSizeAndRefFlag rowBase: rowBaseOop rowInsertRemove: rowInsertRemoveOop rowInsertRemoveCount: rowInsertRemoveCountOop | base insertRemove jLimiT substBase insert remove subst removeBase insertBase insertRemoveCount additionalMultiInsertRemoveCost myPoints otherPoints myVectors otherVectors rowInsertRemoveCount mySquaredLengths otherSquaredLengths myAngles otherAngles rowBase rowInsertRemove myPointsSize otherPointsSize myVectorsSize otherVectorsSize mySquaredLengthsSize otherSquaredLengthsSize rowBaseSize maxDist maxSize forReference jM1 iM1 iM1T2 jM1T2 | self var: #myPoints declareC: 'int * myPoints'. self var: #otherPoints declareC: 'int * otherPoints'. self var: #myVectors declareC: 'int * myVectors'. self var: #otherVectors declareC: 'int * otherVectors'. self var: #mySquaredLengths declareC: 'int * mySquaredLengths'. self var: #otherSquaredLengths declareC: 'int * otherSquaredLengths'. self var: #myAngles declareC: 'int * myAngles'. self var: #otherAngles declareC: 'int * otherAngles'. self var: #rowBase declareC: 'int * rowBase'. self var: #rowInsertRemove declareC: 'int * rowInsertRemove'. self var: #rowInsertRemoveCount declareC: 'int * rowInsertRemoveCount'. self primitive: 'primSameClassAbsoluteStrokeDistanceMyPoints_otherPoints_myVectors_otherVectors_mySquaredLengths_otherSquaredLengths_myAngles_otherAngles_maxSizeAndReferenceFlag_rowBase_rowInsertRemove_rowInsertRemoveCount' parameters: #(#Oop #Oop #Oop #Oop #Oop #Oop #Oop #Oop #SmallInteger #Oop #Oop #Oop) receiver: #Oop. interpreterProxy failed ifTrue: [self msg: 'failed 1'. ^ nil]. interpreterProxy success: (interpreterProxy isWords: myPointsOop) & (interpreterProxy isWords: otherPointsOop) & (interpreterProxy isWords: myVectorsOop) & (interpreterProxy isWords: otherVectorsOop) & (interpreterProxy isWords: mySquaredLengthsOop) & (interpreterProxy isWords: otherSquaredLengthsOop) & (interpreterProxy isWords: myAnglesOop) & (interpreterProxy isWords: otherAnglesOop) & (interpreterProxy isWords: rowBaseOop) & (interpreterProxy isWords: rowInsertRemoveOop) & (interpreterProxy isWords: rowInsertRemoveCountOop). interpreterProxy failed ifTrue: [self msg: 'failed 2'. ^ nil]. interpreterProxy success: (interpreterProxy is: myPointsOop MemberOf: 'PointArray') & (interpreterProxy is: otherPointsOop MemberOf: 'PointArray'). interpreterProxy failed ifTrue: [self msg: 'failed 3'. ^ nil]. myPoints _ interpreterProxy firstIndexableField: myPointsOop. otherPoints _ interpreterProxy firstIndexableField: otherPointsOop. myVectors _ interpreterProxy firstIndexableField: myVectorsOop. otherVectors _ interpreterProxy firstIndexableField: otherVectorsOop. mySquaredLengths _ interpreterProxy firstIndexableField: mySquaredLengthsOop. otherSquaredLengths _ interpreterProxy firstIndexableField: otherSquaredLengthsOop. myAngles _ interpreterProxy firstIndexableField: myAnglesOop. otherAngles _ interpreterProxy firstIndexableField: otherAnglesOop. rowBase _ interpreterProxy firstIndexableField: rowBaseOop. rowInsertRemove _ interpreterProxy firstIndexableField: rowInsertRemoveOop. rowInsertRemoveCount _ interpreterProxy firstIndexableField: rowInsertRemoveCountOop. "PointArrays" myPointsSize _ (interpreterProxy stSizeOf: myPointsOop) bitShift: -1. otherPointsSize _ (interpreterProxy stSizeOf: otherPointsOop) bitShift: -1. myVectorsSize _ (interpreterProxy stSizeOf: myVectorsOop) bitShift: -1. otherVectorsSize _ (interpreterProxy stSizeOf: otherVectorsOop) bitShift: -1. "IntegerArrays" mySquaredLengthsSize _ interpreterProxy stSizeOf: mySquaredLengthsOop. otherSquaredLengthsSize _ interpreterProxy stSizeOf: otherSquaredLengthsOop. rowBaseSize _ interpreterProxy stSizeOf: rowBaseOop. interpreterProxy success: rowBaseSize = (interpreterProxy stSizeOf: rowInsertRemoveOop) & (rowBaseSize = (interpreterProxy stSizeOf: rowInsertRemoveCountOop)) & (rowBaseSize > otherVectorsSize). interpreterProxy failed ifTrue: [self msg: 'failed 4'. ^ nil]. interpreterProxy success: mySquaredLengthsSize >= (myVectorsSize - 1) & (myPointsSize >= myVectorsSize) & (otherSquaredLengthsSize >= (otherVectorsSize - 1)) & (otherPointsSize >= otherVectorsSize) & ((interpreterProxy stSizeOf: myAnglesOop) >= (myVectorsSize - 1)) & ((interpreterProxy stSizeOf: otherAnglesOop) >= (otherVectorsSize - 1)). interpreterProxy failed ifTrue: [self msg: 'failed 5'. ^ nil]. "maxSizeAndRefFlag contains the maxium feature size (pixel) and also indicates whether the reference flag (boolean) is set. Therefore the maximum size is moved to the left and the reference flag is stored in the LSB. Note: This is necessary to avoid more than 12 primitive parameters" forReference _ maxSizeAndRefFlag bitAnd: 1. maxSize _ maxSizeAndRefFlag bitShift: -1. maxDist _ 1 bitShift: 29. forReference ifTrue: [additionalMultiInsertRemoveCost _ 0] ifFalse: [additionalMultiInsertRemoveCost _ maxSize * maxSize bitShift: -10]. "C indices!!!!" rowBase at: 0 put: 0. rowInsertRemove at: 0 put: 0. rowInsertRemoveCount at: 0 put: 2. insertRemove _ 0 - additionalMultiInsertRemoveCost. jLimiT _ otherVectorsSize. otherPointsSize >= (jLimiT - 1) & (otherSquaredLengthsSize >= (jLimiT - 1)) ifFalse: [^ interpreterProxy primitiveFail]. 1 to: jLimiT do: [:j | jM1 _ j - 1. insertRemove _ insertRemove + ((otherSquaredLengths at: jM1) + (self cSquaredDistanceFrom: (otherPoints + (jM1 bitShift: 1)) to: myPoints) bitShift: -7) + additionalMultiInsertRemoveCost. rowInsertRemove at: j put: insertRemove. rowBase at: j put: insertRemove * j. rowInsertRemoveCount at: j put: j + 1]. insertRemove _ (rowInsertRemove at: 0) - additionalMultiInsertRemoveCost. 1 to: myVectorsSize do: [:i | iM1 _ i - 1. iM1T2 _ iM1 bitShift: 1. substBase _ rowBase at: 0. insertRemove _ insertRemove + ((mySquaredLengths at: iM1) + (self cSquaredDistanceFrom: (myPoints + iM1T2) to: otherPoints) bitShift: -7) + additionalMultiInsertRemoveCost. rowInsertRemove at: 0 put: insertRemove. rowBase at: 0 put: insertRemove * i. rowInsertRemoveCount at: 0 put: i + 1. jLimiT _ otherVectorsSize. 1 to: jLimiT do: [:j | jM1 _ j - 1. jM1T2 _ jM1 bitShift: 1. removeBase _ rowBase at: j. insertBase _ rowBase at: jM1. remove _ (mySquaredLengths at: iM1) + (self cSquaredDistanceFrom: (myPoints + iM1T2) to: (otherPoints + (j bitShift: 1))) bitShift: -7. (insertRemove _ rowInsertRemove at: j) = 0 ifTrue: [removeBase _ removeBase + remove] ifFalse: [removeBase _ removeBase + insertRemove + (remove * (rowInsertRemoveCount at: j)). remove _ remove + insertRemove]. insert _ (otherSquaredLengths at: jM1) + (self cSquaredDistanceFrom: (otherPoints + jM1T2) to: (myPoints + (i bitShift: 1))) bitShift: -7. (insertRemove _ rowInsertRemove at: jM1) = 0 ifTrue: [insertBase _ insertBase + insert] ifFalse: [insertBase _ insertBase + insertRemove + (insert * (rowInsertRemoveCount at: jM1)). insert _ insert + insertRemove]. forReference ifTrue: [substBase _ maxDist] ifFalse: [subst _ (self cSquaredDistanceFrom: (otherVectors + jM1T2) to: (myVectors + iM1T2)) + (self cSquaredDistanceFrom: (otherPoints + jM1T2) to: (myPoints + iM1T2)) * (16 + (self cSubstAngleFactorFrom: (otherAngles at: jM1) to: (myAngles at: iM1))) bitShift: -11. substBase _ substBase + subst]. (substBase <= removeBase and: [substBase <= insertBase]) ifTrue: [base _ substBase. insertRemove _ 0. insertRemoveCount _ 1] ifFalse: [removeBase <= insertBase ifTrue: [base _ removeBase. insertRemove _ remove + additionalMultiInsertRemoveCost. insertRemoveCount _ (rowInsertRemoveCount at: j) + 1] ifFalse: [base _ insertBase. insertRemove _ insert + additionalMultiInsertRemoveCost. insertRemoveCount _ (rowInsertRemoveCount at: jM1) + 1]]. substBase _ rowBase at: j. rowBase at: j put: (base min: maxDist). rowInsertRemove at: j put: (insertRemove min: maxDist). rowInsertRemoveCount at: j put: insertRemoveCount]. insertRemove _ rowInsertRemove at: 0]. ^ base asOop: SmallInteger ! ! !GeniePlugin methodsFor: 'version' stamp: 'NS 7/12/2001 11:54'! majorNO ^ 2! ! !GeniePlugin methodsFor: 'version' stamp: 'NS 7/12/2001 11:54'! minorNO ^ 0! ! !GeniePlugin methodsFor: 'version' stamp: 'sr 6/25/2001 20:39'! primVersionNO "majorNO * 1000 + minorNO" self primitive: 'primVersionNO' parameters: #() receiver: #Oop. ^ (self majorNO * 1000 + self minorNO) asOop: SmallInteger! ! !GeniePlugin class methodsFor: 'check installed plugin' stamp: 'sr 6/25/2001 20:49'! majorNO | no | ^ (no _ self versionNO) ifNotNil: [no // 1000] ! ! !GeniePlugin class methodsFor: 'check installed plugin' stamp: 'sr 6/25/2001 20:50'! minorNO | no | ^ (no _ self versionNO) ifNotNil: [no \\ 1000] ! ! !GeniePlugin class methodsFor: 'check installed plugin' stamp: 'sr 6/25/2001 20:46'! versionNO ^ nil ! ! !GeniePlugin class methodsFor: 'check installed plugin' stamp: 'NS 8/8/2001 15:31'! versionString ^ 'v', (self versionNO / 1000 asFloat) asString! ! !GeniePlugin class methodsFor: 'translation' stamp: 'sr 4/15/2001 17:44'! moduleNameAndVersion "Answer the receiver's module name and version info that is used for the plugin's C code. The default is to append the code generation date, but any useful text is ok (keep it short)" ^ self moduleName, Character space asString, self version, Character space asString, Date today asString! ! !GeniePlugin class methodsFor: 'translation' stamp: 'sr 6/25/2001 20:42'! version "Answer the receiver's version info as String." "Somewhat a hack, but calling class methods from inst methods doesn't result in usable C-code..." | inst | inst _ self new. ^ 'v', inst majorNO asString, '.', inst minorNO asString! ! !PasteUpMorph methodsFor: 'genie-processing' stamp: 'NS 7/9/2001 16:14'! allowsGestureStart: anEvent ^ (anEvent hand allowsGestureStart: anEvent target: self) and: [(self handlesGestureStart: anEvent) and: [(self morphToGrab: anEvent) isNil]]! ! !PluggableCollectionMorph methodsFor: 'accessing' stamp: 'NS 7/5/2001 10:35'! balloonTextSelector ^ balloonTextSelector! ! !PluggableCollectionMorph methodsFor: 'accessing' stamp: 'NS 7/5/2001 10:35'! balloonTextSelector: aSymbol balloonTextSelector _ aSymbol! ! !PluggableCollectionMorph methodsFor: 'private' stamp: 'NS 7/5/2001 10:22'! isBalloonTextAvailable ^ self isModelAvailable and: [self balloonTextSelector notNil].! ! !PluggableCollectionMorph methodsFor: 'private' stamp: 'NS 7/5/2001 10:39'! setModel: anObject collectionOrSelector: aCollectionOrSymbol okaySelector: okaySymbol cancelSelector: cancelSymbol addSelector: addSymbol deleteSelector: deleteSymbol gotoSelector: gotoSymbol menuSelector: menuSymbol changeSelector: changeSymbol valueMorphSelector: valueMorphSymbol keyMorphSelector: keyMorphSymbol objectToStringSelector: objectToStringSymbol releaseSelector: releaseSymbol balloonTextSelector: balloonSymbol self model: anObject. collectionOrSelector _ aCollectionOrSymbol. okaySelector _ okaySymbol. cancelSelector _ cancelSymbol. addSelector _ addSymbol. deleteSelector _ deleteSymbol. gotoSelector _ gotoSymbol. menuSelector _ menuSymbol. changeSelector _ changeSymbol. valueMorphSelector _ valueMorphSymbol. keyMorphSelector _ keyMorphSymbol. objectToStringSelector _ objectToStringSymbol. releaseSelector _ releaseSymbol. balloonTextSelector _ balloonSymbol.! ! !PluggableCollectionMorph methodsFor: 'buttons' stamp: 'NS 7/5/2001 12:06'! addButton ^ (self basicButton label: 'New'; actionSelector: #addAction) setBalloonText: (self balloonText: #addButton).! ! !PluggableCollectionMorph methodsFor: 'buttons' stamp: 'NS 7/5/2001 12:03'! basicButton ^ SimpleButtonMorph new target: self; borderColor: #raised; actWhen: #buttonUp.! ! !PluggableCollectionMorph methodsFor: 'buttons' stamp: 'NS 7/5/2001 12:07'! cancelButton ^ self basicButton label: 'Cancel'; actionSelector: #cancelAction.! ! !PluggableCollectionMorph methodsFor: 'buttons' stamp: 'NS 7/5/2001 12:07'! deleteButton ^ (self basicButton label: 'Del'; actionSelector: #deleteAction) setBalloonText: (self balloonText: #deleteButton).! ! !PluggableCollectionMorph methodsFor: 'buttons' stamp: 'NS 7/5/2001 12:07'! gotoButton ^ (self basicButton label: 'Goto'; actionSelector: #gotoAction) setBalloonText: (self balloonText: #gotoButton).! ! !PluggableCollectionMorph methodsFor: 'buttons' stamp: 'NS 7/5/2001 12:07'! menuButton ^ (self basicButton label: 'M'; actionSelector: #menuAction; actWhen: #buttonDown) setBalloonText: (self balloonText: #menuButton).! ! !PluggableCollectionMorph methodsFor: 'buttons' stamp: 'NS 7/5/2001 12:07'! nextButton ^ (self basicButton label: ' >'; actionSelector: #nextAction) setBalloonText: (self balloonText: #nextButton).! ! !PluggableCollectionMorph methodsFor: 'buttons' stamp: 'NS 7/5/2001 12:07'! okayButton ^ (self basicButton label: 'Close'; actionSelector: #okayAction) setBalloonText: (self balloonText: #okayButton).! ! !PluggableCollectionMorph methodsFor: 'buttons' stamp: 'NS 7/5/2001 12:07'! prevButton ^ (self basicButton label: '< '; actionSelector: #prevAction) setBalloonText: (self balloonText: #prevButton).! ! !PluggableCollectionMorph methodsFor: 'model access' stamp: 'NS 7/5/2001 11:20'! balloonText: aSymbol ^ self isBalloonTextAvailable ifTrue: [self model perform: self balloonTextSelector with: aSymbol with: self].! ! !PluggableCollectionMorph class methodsFor: 'instance creation' stamp: 'NS 7/5/2001 10:37'! model: anObject collectionOrSelector: aCollectionOrSymbol okaySelector: okaySymbol cancelSelector: cancelSymbol addSelector: addSymbol deleteSelector: deleteSymbol gotoSelector: gotoSymbol menuSelector: menuSymbol changeSelector: changeSymbol valueMorphSelector: valueMorphSymbol keyMorphSelector: keyMorphSymbol objectToStringSelector: objectToStringSymbol releaseSelector: releaseSymbol balloonTextSelector: balloonSymbol ^ self new initialize setModel: anObject collectionOrSelector: aCollectionOrSymbol okaySelector: okaySymbol cancelSelector: cancelSymbol addSelector: addSymbol deleteSelector: deleteSymbol gotoSelector: gotoSymbol menuSelector: menuSymbol changeSelector: changeSymbol valueMorphSelector: valueMorphSymbol keyMorphSelector: keyMorphSymbol objectToStringSelector: objectToStringSymbol releaseSelector: releaseSymbol balloonTextSelector: balloonSymbol.! ! !SequenceableCollection methodsFor: 'converting' stamp: 'NS 5/30/2001 20:56'! asPointArray "Answer an PointArray whose elements are the elements of the receiver, in the same order." | pointArray | pointArray _ PointArray new: self size. 1 to: self size do:[:i| pointArray at: i put: (self at: i)]. ^pointArray! ! !CRLookupResult methodsFor: 'private' stamp: 'NS 8/8/2000 16:07'! isAvailable: indexNumber "Does the instance contain at least indexNumber features?" ^ indexNumber notNil and: [self size >= indexNumber]! ! !CRLookupResult methodsFor: 'accessing' stamp: 'NS 8/8/2000 16:16'! add: aCRLookupItem "Add a new lookup item to the result" ^ (self size < self minSize or: [(self distanceAt: self size) > aCRLookupItem distance]) and: [super add: aCRLookupItem. true]! ! !CRLookupResult methodsFor: 'accessing' stamp: 'NS 7/19/2001 10:48'! lookupIndex: anIntegerOrNil "iterator position" ^ (anIntegerOrNil notNil and: [anIntegerOrNil > 0 and: [anIntegerOrNil <= self size]]) and: [lookupIndex _ anIntegerOrNil. true].! ! !CRLookupResult methodsFor: 'accessing' stamp: 'NS 7/19/2001 10:49'! nextDistinctCharMatchRollover: aBoolean "Move the iterator to the next dictinct character" | index | index _ self indexOfDistinctChar: 1 startAt: self lookupIndex. ^ (self lookupIndex: index) or: [aBoolean ifTrue: [self lookupIndex: 1] ifFalse: [self lookupIndex: self size]. false]! ! !CRLookupResult methodsFor: 'accessing' stamp: 'NS 7/19/2001 10:49'! nextMatchRollover: aBoolean "Move the iterator to the next match" ^ (self lookupIndex: self lookupIndex + 1) or: [aBoolean ifTrue: [self lookupIndex: 1] ifFalse: [self lookupIndex: self size]. false]! ! !CRLookupResult class methodsFor: 'instance creation' stamp: 'NS 7/18/2001 15:33'! dictionary: aCRDictionary minSize: anInteger ^ (super new: anInteger) initializeDictionary: aCRDictionary minSize: anInteger! ! !PointArray methodsFor: 'converting' stamp: 'NS 5/30/2001 20:54'! asPointArray ^ self! ! !TheWorldMenu methodsFor: 'construction' stamp: 'NS 11/29/2001 13:27'! helpMenu "Build the help menu for the world." | screenCtrl genieEnabledString | screenCtrl _ ScreenController new. genieEnabledString _ World currentHand isGenieEnabled ifTrue: ['disable'] ifFalse: ['enable']. ^ self fillIn: (self menu: 'help...') from: { {'about this system...'. {Smalltalk. #aboutThisSystem}. 'current version information.'}. {'update code from server'. {Utilities. #updateFromServer}. 'load latest code updates via the internet'}. {'preferences...'. {Preferences. #openPreferencesInspector}. 'view and change various options.'}. {'set language...' . {Project. #chooseNaturalLanguage}. 'choose the language in which tiles should be displayed.'} . nil. {genieEnabledString , ' genie'. {World currentHand. #switchGenieEnabled}. genieEnabledString , ' gesture recognizer for the world''s current hand'}. {'genie gesture dictionaries'. {CRDictionary. #openInstanceBrowserMorph}. 'edit or inspect gesture dictionaries'.}. {'choose genie text dictionary'. {CRDictionary. #chooseTextDictionary}. 'select the dictionary used for text input'.}. {'genie display properties'. {CRDisplayProperties. #openInstanceBrowserMorph}. 'edit or inspect display properies'.}. nil. {'command-key help'. { Utilities . #openCommandKeyHelp}. 'summary of keyboard shortcuts.'}. {'world menu help'. { self . #worldMenuHelp}. 'helps find menu items buried in submenus.'}. "{'info about flaps' . { Utilities . #explainFlaps}. 'describes how to enable and use flaps.'}." {'font size summary' . { Utilities . #fontSizeSummary}. 'summary of names and sizes of available fonts.'}. {'useful expressions' . { Utilities . #openStandardWorkspace}. 'a window full of useful expressions.'}. {'annotation setup...' . { Preferences . #editAnnotations}. 'Click here to get a little window that will allow you to specify which types of annotations, in which order, you wish to see in the annotation panes of browsers and other tools'}. nil. {'graphical imports' . { Smalltalk . #viewImageImports}. 'view the global repository called ImageImports; you can easily import external graphics into ImageImports via the FileList'}. {'standard graphics library' . { ScriptingSystem . #inspectFormDictionary}. 'lets you view and change the system''s standard library of graphics.'}. nil. {'telemorphic...' . {self. #remoteDo}. 'commands for doing multi-machine "telemorphic" experiments'}. {#soundEnablingString . { Preferences . #toggleSoundEnabling}. 'turning sound off will completely disable Squeak''s use of sound.'}. {'definition for...' . { Utilities . #lookUpDefinition}. 'if connected to the internet, use this to look up the definition of an English word.'}. nil. {'set author initials...' . { screenCtrl . #setAuthorInitials }. 'supply initials to be used to identify the author of code and other content.'}. {'vm statistics' . { screenCtrl . #vmStatistics}. 'obtain some intriguing data about the vm.'}. {'space left' . { screenCtrl . #garbageCollect}. 'perform a full garbage-collection and report how many bytes of space remain in the image.'}. } ! ! !WorldState methodsFor: 'update cycle' stamp: 'NS 7/25/2001 16:26'! doOneCycleNowFor: aWorld "Do one cycle of the interactive loop. This method is called repeatedly when the world is running." | recognizing | recognizing _ false. self flag: #bob. "need to consider remote hands in lower worlds" "process user input events" LastCycleTime _ Time millisecondClockValue. self handsDo: [:h | ActiveHand _ h. h processEvents. h isGenieRecognizing ifTrue: [recognizing _ h giveGenieChanceToEscape not]. ActiveHand _ nil ]. "the default is the primary hand" ActiveHand _ self hands first. "The gesture recognizer needs enough points to be accurate. Therefore morph stepping is disabled while capturing points for the recognizer" recognizing ifFalse: [aWorld runStepMethods. "there are currently some variations here" self displayWorldSafely: aWorld]. ! ! PluggableCollectionMorph class removeSelector: #model:collectionOrSelector:okaySelector:cancelSelector:addSelector:deleteSelector:gotoSelector:menuSelector:changeSelector:valueMorphSelector:keyMorphSelector:objectToStringSelector:releaseSelector:! PluggableCollectionMorph removeSelector: #setModel:collectionOrSelector:okaySelector:cancelSelector:addSelector:deleteSelector:gotoSelector:menuSelector:changeSelector:valueMorphSelector:keyMorphSelector:objectToStringSelector:releaseSelector:! !GeniePlugin class reorganize! ('check installed plugin' majorNO minorNO versionNO versionString) ('translation' moduleNameAndVersion version) ! !GeniePlugin reorganize! ('computation' cSquaredDistanceFrom:to: cSubstAngleFactorFrom:to: primSameClassAbsoluteStrokeDistanceMyPoints:otherPoints:myVectors:otherVectors:mySquaredLengths:otherSquaredLengths:myAngles:otherAngles:maxSizeAndReferenceFlag:rowBase:rowInsertRemove:rowInsertRemoveCount:) ('version' majorNO minorNO primVersionNO) ! FillInTheBlankMorphWithCharMenu class removeSelector: #defaultAnswerExtent! FillInTheBlankMorphWithCharMenu class removeSelector: #request:initialAnswer:centerAt:inWorld:onCancelReturn:! FillInTheBlankMorphWithCharMenu removeSelector: #initialize! CRStrokeMorph removeSelector: #ensureOpenAndAddPoint:! CRStrokeFeature initialize! !CRStrokeFeature class reorganize! ('accessing' createTempArrays: rowBase rowInsertRemove rowInsertRemoveCount strokeReferenceFeature1) ('class initialization' initialize) ('instance creation' new) ('constants' capturedPointsReduceAngle) ('primitive test' testOriginalPerformance testPerformanceBlock:dictionary:iterations:output: testPerformanceMetrics:dictionaryName:iterations:output: testPerformanceMetrics:iterations:output: testPerformancePrimitive: testPerformancePrimitive:iterations:output: testPrimitive testPrimitive:original:dictionary:relDiffLimit:detailed: testPrimitiveDictionaryName:forReference:detailed: testPrimitivePerformance testPrimitiveWithDetails:) ('update' updateToVersion2000) ! CRStrokeFeature removeSelector: #calcRelativePoints! CRStrokeFeature removeSelector: #relativePoints! CRStrokeFeature removeSelector: #relativePoints:! CRStrokeFeature removeSelector: #sameClassAcuteAngleDistance:! CRStrokeFeature removeSelector: #sameClassAngleDistance:! CRStrokeFeature removeSelector: #sameClassShapeDistance:! CRStrokeFeature removeSelector: #sameClassStartEndDistance:! CRStrokeFeature removeSelector: #sameClassStrokeDistance:! CRStrokeFeature removeSelector: #sameClassStrokeDistance:max:! CRStrokeFeature removeSelector: #sameClassStrokeSizeDistance:! CRStrokeFeature removeSelector: #sameClassSymmetricStrokeDistance:! CRStrokeFeature removeSelector: #setSecondaryValues:! !CRStrokeFeature reorganize! ('private' absAngleSum: acuteAngleCount: acuteAnglePosition: addSecondaryValues: angleDifferenceFrom:to: areSecondaryValuesAvailable calcAbsAngleSum calcAcuteAngleArray calcAcuteAngleArray:squaredLengths: calcAngles calcEndPoint calcGlobalAngles calcNegAngleSum calcPoints calcPosAngleSum calcPosAngleSum:squaredLengths:maxAngle: calcSize calcSquaredLengths calcSquaredWeightedLengths directionVectors: drawPoints:on:size:topLeft:relative:aspectRatio: drawPoints:on:topLeft:scaleFactor: endPoint: extent: fastLen: globalAngles: maxStrokeDistance: negAngleSum: outputFactorForSize:relative:aspectRatio: outputShiftVectorForSize:relative:aspectRatio: points: posAngleSum: resetSecondaryValues size: squaredDistanceFrom:to: squaredWeightedLengths: startPoint: substAngleFactorFrom:to: weightedStartEndAngle:squaredLength:) ('initialize-release' initialize) ('testing' hasSomeCapturedPoints isCacheFull isStroke) ('accessing' absAngleSum acuteAngleCount acuteAnglePosition angles calcAndSetSecondaryValues capturedPoints capturedPoints: directionVectors drawCapturedPointsOn:size:topLeft:relative:properties: drawFeatureOn:size:topLeft:relative:properties: endPoint extent fastGlobalAngles fastPoints fastSquaredWeightedLengths fillCache fillCacheIfNotFull globalAngles hotspot hotspot: maxStrokeDistance negAngleSum originalStartPoint originalStartPoint: points posAngleSum reduceCapturedPoints relativeSize releaseCache removeCapturedPoints setSecondaryValuesToAvg: setValuesToAvg: size squaredLengths squaredWeightedLengths startPoint strokeSize updateMaxStrokeDistance) ('private comparing' origSameClassAbsoluteStrokeDistance:forReference: primSameClassAbsoluteStrokeDistance:forReference: sameClassAbsoluteStrokeDistance:forReference: sameClassAcuteAngleDistance:maxNormDist: sameClassAngleDistance:maxNormDist: sameClassShapeDistance:maxNormDist: sameClassStartEndDistance:maxNormDist: sameClassStrokeDistance:maxDist:maxNormDist: sameClassStrokeDistance:maxNormDist: sameClassSymmetricStrokeDistance:maxNormDist:) ('update' updateToVersion2000) ('plugin control' callPrimSameClassAbsoluteStrokeDistanceMyPoints:otherPoints:myVectors:otherVectors:mySquaredLengths:otherSquaredLengths:myAngles:otherAngles:maxSizeAndReferenceFlag:rowBase:rowInsertRemove:rowInsertRemoveCount:) ('objects from disk' readDataFrom:size: storeDataOn:) ! CRRecognizer removeSelector: #addDirectionTowardsPoint:minSquaredLength:minAngle:last:! CRRecognizer removeSelector: #directionPoints! CRRecognizer removeSelector: #directionPoints:! CRRecognizer removeSelector: #directionVectors! CRRecognizer removeSelector: #directions! CRRecognizer removeSelector: #directions:! CRRecognizer removeSelector: #ensureMaxDirectionLength:! CRRecognizer removeSelector: #resetTempCollections! Object subclass: #CRRecognizer instanceVariableNames: 'dictionary displayProperties points coordinates lastPoint startTime endTime escapePossible isEchoEnabled echo rasteredPoints currentDisplayProperties lastAddedPoint ' classVariableNames: 'CharacterDictionary ' poolDictionaries: '' category: 'Genie-Engine'! CRFeature removeSelector: #acuteAngleDistance:! CRFeature removeSelector: #angleDistance:! CRFeature removeSelector: #sameClassAcuteAngleDistance:! CRFeature removeSelector: #sameClassAngleDistance:! CRFeature removeSelector: #sameClassShapeDistance:! CRFeature removeSelector: #sameClassStartEndDistance:! CRFeature removeSelector: #sameClassStrokeDistance:! CRFeature removeSelector: #sameClassStrokeSizeDistance:! CRFeature removeSelector: #sameClassSymmetricStrokeDistance:! CRFeature removeSelector: #shapeDistance:! CRFeature removeSelector: #sizeDistance:! CRFeature removeSelector: #startEndDistance:! CRFeature removeSelector: #strokeDistance:! CRFeature removeSelector: #strokeSizeDistance:! CRFeature removeSelector: #symmetricStrokeDistance:! CRFeature removeSelector: #timeDistance:! !CREcho reorganize! ('initialize-release' initialize) ('accessing' addPoint: delete) ! CRDictionary removeSelector: #addDistances:to:weight:eliminatedDist:! CRDictionary removeSelector: #eliminateDistancesFrom:conditionCollection:limit:minSize:eliminatedDist:! CRDictionary removeSelector: #localLookup:minResultSize:symmetric:maxSpeed:! CRDictionary removeSelector: #lookup:minResultSize:symmetric:maxSpeed:includeParents:! CRDictionary removeSelector: #putAcuteAngleDistancesFrom:to:into:eliminateCollection:limit:! CRDictionary removeSelector: #putAngleDistancesFrom:to:into:eliminateCollection:limit:! CRDictionary removeSelector: #putShapeDistancesFrom:to:into:eliminateCollection:limit:! CRDictionary removeSelector: #putSizeDistancesFrom:to:into:eliminateCollection:limit:! CRDictionary removeSelector: #putStartEndDistancesFrom:to:into:eliminateCollection:limit:! CRDictionary removeSelector: #putStrokeDistancesFrom:to:into:eliminateCollection:limit:! CRDictionary removeSelector: #putStrokeSizeDistancesFrom:to:into:eliminateCollection:limit:! CRDictionary removeSelector: #putSymmetricStrokeDistancesFrom:to:into:eliminateCollection:limit:! CRDictionary removeSelector: #putTimeDistancesFrom:to:into:eliminateCollection:limit:! !CRDictionary reorganize! ('private' addIndirectParentsTo: addToInvertedDictionaryFeature:char: capturedPoints: createDistinctName:collection: createParentsCollection dictionary dictionary: getNameAndVersionFromString: getNameAndVersionString invertedDictionary invertedDictionary: localLookup:symmetric:maxSpeed:result: parameters: reduceCapturedPoints removeCapturedPoints replaceBy: versionNO:) ('accessing dictionaries' addChild: atChar: atChar:ifAbsent: atCharString: atFeature: atFeature:ifAbsent: atFeature:put: charSize featureSize featuresAndCharsDo: includesChar: includesFeature: indirectParentIncludesFeature: isEmpty notEmpty removeChar: removeChar:ifAbsent: removeFeature: removeFeature:ifAbsent: renameChar:to: selfOrIndirectParentIncludesFeature: size updateInvertedDictionary) ('views' asCloseableMorph asMorph browserAppModel newBrowser newCloseableMorph newMorph openBrowser openMorph) ('updating' update:) ('accessing' addParent: addParentName: capturedPoints exportedName exportedName: exportedNameAsString fillFeaturesCache fillFeaturesCacheIfNotFull getParentsAndErrorForString: indirectParentCount indirectParents lookup: lookup:minResultSize: lookup:minResultSize:symmetric: lookup:minResultSize:symmetric:maxSpeed: maxMultipleDefinitions maxMultipleDefinitions: name name: name:makeDistinct: nameAsString parameters parentCount parentFromName: parentMenuSelector: parents parents: parentsAsString parentsFromString: recalculate releaseFeaturesCache removeAllParents removeParent: storeAllCapturedPoints storeNoCapturedPoints storeReducedCapturedPoints totalMaxMultipleDefinitions totalParentCount totalSize updateMaxMultipleDefinitions updateMaxMultipleDefinitions: versionNO) ('initialize-release' initialize) ('copying' copy) ('objects from disk' basicReadDataFrom:size: basicStoreDataOn: readDataFrom:size: storeDataOn: storeParents storeParents: storedName storedName:) ('testing' hasAllCapturedPoints hasNoCapturedPoints hasParents hasReducedCapturedPoints isActive) ('comparing' <) ('update' updateToVersion2000) ! "Postscript: Make sure that all the CRStrokeFeature in the system use IntegerArray resp. PointArray wherever possible. Note: When saved dictionaries are loaded into the system, they are updated automatically" CRStrokeFeature initialize. CRStrokeFeature updateToVersion2000. CRDictionary updateToVersion2000. "Is now implemented as InterpreterPlugin>>msg:" Smalltalk removeSelector: #(LargeIntegersPlugin #msg:)!