TweetFollow Us on Twitter

June 96 - Adding Custom Data to QuickDraw 3D Objects

Adding Custom Data to QuickDraw 3D Objects

PABLO FERNICOLA, NICK THOMPSON and KENT DAVIDSON

Custom attributes and elements provide a way to attach data such as scaling information, sound, and strings to QuickDraw 3D objects. In this article we explain how to create and attach custom attributes and elements. We illustrate the process by showing you how to attach a string containing a World Wide Web URL to a QuickDraw 3D object to enable 3D navigation through the Web. We also describe six new custom elements with implementations included on this issue's CD.

In QuickDraw 3D, attribute objects (known more simply as attributes) generally store information about the surface properties of objects in a model, such as color and transparency. QuickDraw 3D defines 12 basic attribute types, and it also allows you to define custom attribute and element types so that you can attach data different from the predefined types to QuickDraw 3D objects. Your custom data need not apply to the appearance of objects or to how objects are drawn, although it can.

For example, with custom attributes and elements you can add scaling information, directional information, or sound to objects in your 3D scene. You can add a string containing a name for a QuickDraw 3D object, so that you can refer to that object by name and control it from a scripting language in your application. Your application can enable users to navigate through the World Wide Web in 3D, by attaching a URL to a QuickDraw 3D object, as illustrated by the code discussed in this article and included on this issue's CD. These are just a few of the ways you can extend the functionality of QuickDraw 3D and add value to your 3D application by adding custom data to objects.

Before we explain and illustrate how to create custom attributes and elements, and how to attach them to QuickDraw 3D objects, we'll look at how attributes and elements relate to each other and to other QuickDraw 3D objects. To get the most from this article, you should already be familiar with the basics of QuickDraw 3D, as presented in the previous develop articles "QuickDraw 3D: A New Dimension for Macintosh Graphics" (Issue 22) and "The Basics of QuickDraw 3D Geometries" (Issue 23). The book 3D Graphics Programming With QuickDraw 3D, included on this issue's CD, provides complete documentation for the QuickDraw 3D programming interfaces.

ABOUT ATTRIBUTES AND ELEMENTS

Attributes and elements are types of QuickDraw 3D objects used to store information about objects they're attached to. Each consists of a type and some associated data. You apply attributes and elements to objects by creating an instance of a specific type of attribute or element, defining its data, adding it to a set, and then attaching the set to an object (if the set isn't already attached).

Note that attributes and elements are attached to objects, as opposed to simply being added to a group. The reason for binding data to objects is that both QuickDraw 3D and the 3DMF format maintain a strong data encapsulation model. For example, this allows QuickDraw 3D objects to be moved from file to file without losing data.

ATTRIBUTES AND ELEMENTS IN THE QUICKDRAW 3D HIERARCHY

To better understand how attributes and elements relate to each other and to other QuickDraw 3D objects, take a look at the partial class hierarchy shown in Figure 1. As you can see, an attribute is actually a type of element (that is, it's a subclass of the Element class, TQ3ElementObject). An element is any QuickDraw 3D object that can be part of a set. In contrast with shared objects (objects of the class TQ3SharedObject), elements aren't shared (that is, they can't be referenced by multiple objects or the application at the same time) and are always removed from memory whenever they're disposed of. An attribute has all of these properties but also can be inherited by subclasses of the object it's attached to.

Figure 1. Partial QuickDraw 3D class hierarchy, showing set and attribute set attachment

Custom data to be attached to an object can be stored in an element or an attribute. So how do you decide which to use? Use an attribute when you want your custom data to be inherited. For example, suppose we create a custom attribute named Temperature and we want to be able to assign a different temperature to an entire geometry, a face, or a vertex. During a view traversal loop, our attribute will be inherited along with the other attributes. This becomes extremely important with the introduction of plug-in renderers, which will be available in a future QuickDraw 3D release. A particular renderer might take advantage of this inherited attribute by coloring each vertex according to the temperature inherited.

SETS AND ATTRIBUTE SETS

We mentioned earlier that attributes and elements are usually collected in sets. A set is an instance of the Set class (TQ3SetObject), which in turn is a subclass of the Shared class (TQ3SharedObject), as shown in Figure 1. A set collects zero or more different elements or attributes and their associated data; it can contain only one element or attribute of a given type. An attribute set is a type of set; in fact, TQ3AttributeSet is the only subclass of the class TQ3SetObject. An attribute set has all the properties of a set but also allows inheritance.

Both elements and attributes can be collected in sets and attribute sets. Since the AttributeSet class is derived from the Set class, you can call Q3Set_XX on an attribute set, but you can't call Q3AttributeSet_XX on a set. In the text that follows, be sure to pay attention to whether we're talking about sets or attribute sets; we don't use the terms interchangeably.

Sets and attribute sets can't be attached to just any QuickDraw 3D object, but only to those objects for which it makes sense to store additional data in this way. Attribute sets can be attached to view objects, group objects, and geometric objects, plus most of the parts of a geometric object: faces, vertexes, mesh edges, and mesh corners. (See "How to Attach Attribute Sets" for details.) In contrast, sets can be attached only to objects in the Shape class or subclasses of the Shape class. (Attaching a set to a shape is fairly straightforward; we give an example of how to do this later, in Listing 3.) The Shape class actually has a class field of type set, meaning that any class derived from Shape has a set object. The Geometry class has a class field of type set (inherited from the Shape class) plus a class field of type attributeSet, meaning that any class derived from Geometry has both a set object and an attribute set object.


    HOW TO ATTACH ATTRIBUTE SETS

    It may be news to you that attribute sets can be attached to views or groups, because how to do this is less than obvious. We'll tell you how.

    To attach an attribute set to a view object, use the Q3View_GetDefaultAttributeSet routine to get the default attribute set (all view objects have one), and then use Q3AttributeSet_Add to add attributes to that set. For example, the following code shows how to apply a default specular color to all objects submitted to a view.

    Q3View_GetDefaultAttributeSet(theDocument
       ->theView, &viewSet);
    Q3AttributeSet_Add(viewSet, 
       kQ3AttributeTypeSpecularColor, &clearColor);
    Q3Object_Dispose(viewSet);
    
    You can still override the default behavior of the view by attaching attributes to objects before submitting them. If you write the view hints out in 3DMF format using the QuickDraw 3D API, the attribute set for the view will also be written out. You can preserve these settings by looking in and using the view hints when you read the 3DMF data back in.

    To attach an attribute set to a group object, just add the attribute set to the group before you add the object you want it to be applied to.

    Attaching an attribute set to a geometric object or a part of a geometric object is much more obvious, so we won't go into details here. Later in this article, Listing 2 gives an example of how to do it.


Currently, the renderers shipped with QuickDraw 3D ignore custom data attached to shape objects, but when plug-in renderers become available, they may pay attention to such data and use it to control certain rendering features. For example, a ray tracer renderer may need custom data about surfaces to render them with bump mapping.

ATTRIBUTES AND INHERITANCE

When the objects in a view are rendered, attributes attached to the objects are applied according to a strict hierarchy. The attribute sets of objects higher in the view hierarchy are inherited by objects below them, unless some other attribute set overrides them. Inheritance proceeds from view to group to geometric object to face to mesh edge to vertex to mesh corner. In other words, in the hierarchy, view attributes are always inherited unless a group contains overriding attributes; group attributes can be overridden by geometric object attributes, which can be overridden by face attributes, and so on.

When you define a custom attribute, you can specify that you want it to be inherited by including an attribute inheritance method in your metahandler. (More on metahandlers later.) Inheritance happens when you call Q3AttributeSet_Inherit:

TQ3Status Q3AttributeSet_Inherit(TQ3AttributeSet parent, 
                              TQ3AttributeSet child, TQ3AttributeSet result);
This call takes three attribute sets: the parent, the child, and a result attribute set to store results in, which becomes the effective attribute set after inheritance. During inheritance, any attribute in the parent that's not in the child is copied into the result, and all child attributes are copied into the result, as illustrated by the example in Figure 2. As mentioned earlier, only attributes can be inherited; elements, such as the name element "Jane" in this example, can exist in an attribute set but aren't inherited.

Figure 2.Attribute inheritance

WORKING WITH CUSTOM ATTRIBUTES AND ELEMENTS

Now that you have a sense of how custom attributes and elements relate to each other and to other QuickDraw 3D objects, we'll outline how you define, register, and attach your custom data to the QuickDraw 3D objects of your choice. We'll further illustrate the process later in our example of attaching a URL to a QuickDraw 3D object.

DEFINING AND REGISTERING YOUR CUSTOM DATA

To define a custom attribute or element type, you need to provide a definition of the data associated with that type and write a metahandler to define a set of attribute- or element-handling methods. Once you've defined and registered your custom attribute or element type, you manipulate objects of that type exactly as you manipulate the standard QuickDraw 3D attributes. For example, you create a new attribute set by calling Q3AttributeSet_New, and you add custom attributes to the attribute set by calling Q3AttributeSet_Add. Finally, you attach the attribute set to an object by calling an appropriate QuickDraw 3D routine.

Before you can use your custom element or attribute, you must register it with QuickDraw 3D by calling Q3ElementClass_Register or Q3AttributeClass_Register:

TQ3ObjectClass Q3ElementClass_Register(TQ3ElementType elementType,
   const char *name, unsigned long sizeOfElement, TQ3MetaHandler metaHandler);
TQ3ObjectClass Q3AttributeClass_Register(TQ3AttributeType   attributeType,
   const char *name, unsigned long sizeOfElement, TQ3MetaHandler metaHandler);
The functions take these parameters:
  • elementType (or attributeType) -- The type constant used in the binary metafile and in accessing your element (or attribute) from a set.

  • name -- The string constant used to write your custom element or attribute in a text metafile. You should register your attribute or element types and names with Apple's Developer Support Center to prevent name space collisions. In general, you should name your custom elements and attributes in the form "Company:DataType"; for instance, if you work at Sun, you might name an attribute "Sun:JavaCode."

  • sizeOfElement -- The memory size that your element or attribute uses internally. QuickDraw 3D needs to know this when copying your element or attribute, because the data describing the element or attribute is copied from the public side of the API to internal storage.

  • metaHandler -- A pointer to the metahandler for your element or attribute.
A metahandler is an application-defined function that returns the addresses of the methods associated with the custom attribute or element type. QuickDraw 3D calls these methods at certain times to handle operations on sets and attribute sets that contain your custom data. Particular methods are required for each QuickDraw 3D object type, and QuickDraw 3D asks the metahandler repeatedly for these required methods. Your metahandler should, by default, return NULL for unrecognized methods; this allows Apple to add methods in the future without breaking the implementation of old versions of elements and attributes.

A metahandler can define some or all of the methods indicated by the constants listed below. Custom elements or attributes that are to be read from and written to files should support the I/O methods associated with objects (those methods beginning with "kQ3MethodTypeObject" in the following list). The metahandler can also support all the methods associated with elements (those methods beginning with "kQ3MethodTypeElement" in the list) and attributes (those methods beginning with "kQ3MethodTypeAttribute"). Note that the copy methods always take the source as the first parameter (from) and the destination as the second parameter (to), although what these point to differs for each copy method. All of the following method types are optional. If you supply no method for a particular attribute or element type, your attribute or element will inherit the default behavior of the parent class.

  • kQ3MethodTypeObjectReadData -- Reads the data from a file object, gathers any subobjects, and adds the element to a set.

  • kQ3MethodTypeObjectTraverse -- Calculates the size of the data to be written out, submits any subobjects, and gathers any state needed from the view object.

  • kQ3MethodTypeObjectWrite -- Actually writes the data to the file. Data is written through one of the low-level calls provided by QuickDraw 3D for basic data types. If your data size is always 0, no ObjectWrite method is required.

  • kQ3MethodTypeElementCopyAdd -- Called when an application calls Q3Set_Add or Q3AttributeSet_Add on your element and the element wasn't in the set. The from parameter is whatever the user passes in as the data pointer in Q3Set_Add. The to parameter is a pointer to an uninitialized block of sizeOfElement (from the Register call) bytes. If this method isn't supplied, the default is to copy sizeOfElement bytes from the source to the destination.

  • kQ3MethodTypeElementCopyReplace -- Called when an application calls Q3Set_Add or Q3AttributeSet_Add on your element and the element already exists in the set. The from parameter is whatever the user passes in as the data pointer in Q3Set_Add. The to parameter is a pointer to a block of sizeOfElement bytes that contains the element data to be replaced. You must reuse or delete any data in the destination before copying over it. If this method isn't supplied, the default is to call ElementDelete on the to parameter, then CopyAdd(from, to).

  • kQ3MethodTypeElementCopyGet -- Called when an application calls Q3Set_Get or Q3AttributeSet_Get on your element. The from parameter is a pointer to the block of element data to get. The to parameter is a pointer to whatever the user passes in as the data pointer in Q3Set_Get. If this method isn't supplied, the default is to copy sizeOfElement bytes from the source to the destination.

  • kQ3MethodTypeElementCopyDuplicate -- Called when an application calls Q3Object_Duplicate on a set or attribute set, or duplicates an object containing a set. The from parameter is a pointer to the block of element data to duplicate. The to parameter is a pointer to an uninitialized block of sizeOfElement bytes. If your element contains objects, call Q3Object_Duplicate to create an identical copy. If this method isn't supplied, the default is to copy sizeOfElement bytes from the source to the destination.

  • kQ3MethodTypeElementDelete -- Called when an application deletes a set containing your element, or clears your element with Q3Set_Clear or Q3AttributeSet_Clear. It takes a pointer to the block of element data. It should deallocate any data in your custom element. If this method isn't supplied, the default is a no-op.

  • kQ3MethodTypeAttributeInherit -- Your metahandler should return a TQ3Boolean value for this method. Returning kQ3True indicates that this attribute should be inherited in the hierarchy, kQ3False that it should not. The default is kQ3False.

  • kQ3MethodTypeAttributeCopyInherit -- Called when your attribute is inherited in the view stack (during rendering) or when the user calls Q3AttributeSet_Inherit with an attribute set containing your attribute. The from parameter is a pointer to the block of attribute data to inherit. The to parameter is a pointer to an uninitialized block of sizeOfElement bytes. The semantics of this call are similar to kQ3MethodTypeElementCopyDuplicate, although you should avoid duplicating data unless required. For example, if your attribute contains pointers to shared objects, you should copy them by calling Q3Shared_GetReference instead of Q3Object_Duplicate. If this method isn't supplied, the default is to copy sizeOfElement bytes from the source to the destination. This method should be implemented to be as fast as possible, as it occurs during rendering.
Listing 1 shows a typical metahandler for a custom element in QuickDraw 3D. Take a look at the QuickDraw 3D header file QD3DIO.h to see the object methods and at QD3DSet.h to see the element and attribute methods.

Listing 1. A typical metahandler for a custom element

TQ3FunctionPointer MyMetaHandler(TQ3MethodType methodType) 
{
   switch (methodType) {
      case kQ3MethodTypeObjectTraverse:
         return (TQ3FunctionPointer) MyElementTraverse;
      case kQ3MethodTypeObjectWrite:   
         return (TQ3FunctionPointer) MyElementWrite;
      case kQ3MethodTypeObjectReadData:   
         return (TQ3FunctionPointer) MyElementReadData;
      case kQ3MethodTypeElementCopyAdd:   
         return (TQ3FunctionPointer) MyElementCopyAdd;
      case kQ3MethodTypeElementCopyReplace:
         return (TQ3FunctionPointer) MyElementCopyReplace;
      case kQ3MethodTypeElementCopyGet:   
         return (TQ3FunctionPointer) MyElementCopyGet;
      case kQ3MethodTypeElementCopyDuplicate:
         return (TQ3FunctionPointer) MyElementCopyDuplicate;
      case kQ3MethodTypeElementDelete:   
         return (TQ3FunctionPointer) MyElementDelete;
      default:
         return (TQ3FunctionPointer) NULL;
   }
}

ATTACHING YOUR CUSTOM DATA

Now we'll show you how to add the custom data you've defined to a set or an attribute set and then attach that set or attribute set to an object. Note that when you want to attach custom data to a geometric object or some part of a geometric object, you actually have a choice of where to attach the data. You can add the data to an attribute set and attach it to the geometry or some part of the geometry, or you can add the same data to a set and attach that set to a shape, since the geometry inherits from the shape. Where and how data is attached to an object is really up to the semantics of your application. Just be sure to consistently attach data in the same place on all objects, and document what you've done, especially if you want your custom element or attribute to be used by other developers.

To illustrate this concept, Listing 2 creates a new attribute set, adds our custom data to the attribute set, and attaches the attribute set to a mesh vertex. This is a fine way to customize a geometric object or some part of a geometric object. But if you want to add your custom data to some other subclass of the Shape class, you'll want to add the data to a set and attach that set to the shape. Listing 3 does just that.

Listing 2. Attaching an attribute set to a vertex

/* Get the existing attribute set (if any). */
Q3Mesh_GetVertexAttributeSet(mesh, someVertex, &theAttrSet);

/* If there's no attribute set we get back NULL and create one. */
if (theAttrSet == NULL) {
   /* Create a new empty attribute set. */
   theAttrSet = Q3AttributeSet_New();
   if (theAttrSet == NULL)
      return kQ3Failure;
   Q3Mesh_SetVertexAttributeSet(mesh, someVertex, theAttrSet);
}

/* Add the custom data to the attribute set. */
if (Q3AttributeSet_Add(theAttrSet, kMyCustomDataType, &myCustomData) 
      == kQ3Failure) {
   Q3Object_Dispose(theAttrSet);
   return kQ3Failure;
}
Q3Object_Dispose(theAttrSet);
return kQ3Success;

Listing 3. Attaching a set to a shape
/* Get the existing set (if any). */
Q3Shape_GetSet(shape, &theSet);

/* If there's no set, add one. */
if (theSet == NULL) {
   theSet = Q3Set_New();
   if (theSet == NULL)
      return kQ3Failure;
   Q3Shape_SetSet(shape, theSet);
}

/* Add the custom data to the set. */
if (Q3Set_Add(theSet, kMyCustomDataType, &myCustomData) 
      == kQ3Failure) {
   Q3Object_Dispose(theSet);
   return kQ3Failure;
}
Q3Object_Dispose(theSet);
return kQ3Success;

A CASE IN POINT: ATTACHING A URL TO AN OBJECT

Now we're going to illustrate how to define, register, and attach a custom element to a QuickDraw 3D object, and how to extract and use that custom data. Our custom element is a string containing a URL (uniform resource locator, a popular way of specifying the location of an online resource on the Web); we'll attach it to a geometry object. We make it an element rather than an attribute because it doesn't need to be inheritable. When the object we attach the custom element to is read into one of the many viewers that support custom elements, the viewer can communicate through Apple events with applications like Netscape Navigator (TM) (or your favorite Web browser) to produce 3D navigation. A sample application that illustrates the idea is included on this issue's CD. See "3D Web Content Using 3DMF and Netscape Navigator" for more details.

The custom attribute we define and use here, W3Anchor, is one of the six custom elements described later in this article.


    3D WEB CONTENT USING 3DMF AND NETSCAPE NAVIGATOR

    BY JOHN LOUCH

    With the advent of Netscape Navigator 2.0 and its plug-in architecture, you can extend content on the Web to handle multimedia or 3D media. The ease of publishing 3D content will make Apple's 3DMF data format ubiquitous on the Web. A sample Netscape plug-in, Whurlplug, on this issue's CD shows what a 3D plug-in based on QuickDraw 3D might look like.

    Whurlplug uses the QuickDraw 3D Viewer shared library as its interface for displaying 3D Web content. The Viewer gives users of Whurlplug a seamless integration with the current metaphors for handling 3D content on the Mac OS. Whurlplug also tries to use the same human interface metaphors and behaviors as Netscape.

    Whurlplug can be embedded in a Hypertext Markup Language (HTML) page or take over the whole window (as in the URL example in this article). If the plug-in is embedded, it will assume the same background color as the HTML page it's embedded in. Holding down the mouse button on the Viewer toolbar will pop up a menu allowing you to set the Viewer options and save the Web-based 3DMF object to disk, so it's consistent with other elements of the Netscape browser's user interface.

    There are a number of ways to present a 3D scene to a Web user. You can enable the user to fly through a 3D world, or simply to view an HTML page with 3D content. To handle the different ways that Whurlplug might be used, we extended the HTML syntax that the plug-in understands if it's embedded in a page. Here's the Embed command syntax:

    <EMBED src="3dobject.3dmf" WIDTH=100 HEIGHT=200>

    Six more arguments for this extension to HTML can be used in a description of a 3DMF object:

    • ACTIVE -- If this is set to true, the user can examine the 3D object through the controls provided by the QuickDraw 3D Viewer and keyboard navigation. If it's false, the user can interact with the 3D object only if it has URL links to other pages inside it.

    • BGCOLOR -- Allows the page author to set the background color of the plug-in or model to the color supplied. BGCOLOR="#ffffff" would set the background color to white. The string is defined as a number consisting of six hexadecimal digits, each pair of which describes the red, blue, and green components (in that order).

    • SPIN -- If this is set to true, the 3D object will spin about a moving axis defined by Whurlpug; otherwise, the object won't spin.

    • ROTATE -- This also allows the 3D object to spin when viewed, but the page author defines the axis of rotation. The syntax is ROTATE="x y z" where x, y, and z are floating-point values from -180.0 to 180.0 defining the axis of rotation.

    • TOOLBAR -- If this is set to false, the toolbar at the bottom of the viewer isn't shown. The default is true.

    • RENDER -- Tells the plug-in which renderer to use. RENDER=interactive (the default) indicates the interactive software renderer; RENDER=wireframe indicates the wireframe renderer that ships with QuickDraw 3D.

    Whurlplug understands 3D models that have URL or anchor links in them. If the cursor moves over a 3D object that has an anchor link in it, the object flashes red and the URL is displayed in Netscape's toolbar. Clicking on that object causes Netscape to go to that URL, which could be anything from another QuickDraw 3D object to any type of page that Netscape understands. Currently, the only way to add anchors to a QuickDraw 3D object is through the applications BeWhurled (on this issue's CD), 3D World, and Studio Pro Blitz, but the URL example in this article shows how you can add the anchor custom attribute to data in your own 3D application.

    The mime type and subtype for 3DMF are x-world/x-3dmf. The extensions that Whurplug understands are .3dmf, .3dm, .qd3d, and .qd3. Your Web server has to either set the mime type and subtype of 3D files to x-world/x-3dmf or name the files so that the extension is one of those Whurlplug understands.

    Following is a trivially simple HTML description of a Web page that uses this viewer. By the time you read this, there will be (we hope) a number of sites with 3DMF data on their Web pages that can be viewed in Netscape. Check out the QuickDraw 3D Web page for more details.

    <TITLE> A 3D Web page <\TITLE>
    <EMBED src="3dobject.3dmf" WIDTH=200 HEIGHT=200
       SPIN=true ACTIVE=false>
    <P>
    <A HREF="3DObject.3dmf">Click here for a full
       view</A>
    

DEFINING OUR DATA STRUCTURE

We first need to define the internal structure of the data associated with our custom element type. We'll use the W3AnchorData structure, defined like this:
typedef enum W3AnchorOptions {
   kW3AnchorOptionNone      = 0,
   kW3AnchorOptionUseMap   = 1
} W3AnchorOptions;

typedef struct W3AnchorData {
   char               *url;
   TQ3StringObject   description;
   W3AnchorOptions   options;
} W3AnchorData;
The url field is a C string consisting of the URL data. The description object is information that the application must present to users to enable them to decide whether the site or data pointed to by the URL is worth examining (since the process could take some time). Note that since the description is a string object, it can be specified in a script other than Roman. The options field specifies whether the position (x,y) that was clicked should be passed back to the Web viewer.

REGISTERING OUR CUSTOM ELEMENT

Before we can use our custom element, we need to tell QuickDraw 3D that we've defined it, by implementing a registration routine. There may be occasions when we want to recognize a custom element for only a limited period of time, so an unregister routine can also be implemented.

We need to define a couple of parameters before we can register our custom element: an object type, which is a four-character identifier packed into a long word, and a string, which is used to help uniquely identify the element. As mentioned earlier, both of these need to be registered with the Developer Support Center to avoid name space collisions, and each must be unique within their respective name spaces.

#define kElementTypeW3Anchor \ 
   ((TQ3ElementType) Q3_OBJECT_TYPE('w','w','w','a'))
#define kElementNameW3Anchor   "W3Anchor"
Now we register the custom element:
TQ3Status W3Anchor_Register(void)
{
   gW3AnchorClass = Q3ElementClass_Register(kElementTypeW3Anchor,
      kElementNameW3Anchor, sizeof(W3AnchorData),
      W3Anchor_MetaHandler);
   return (gW3AnchorClass == NULL ? kQ3Failure : kQ3Success);
}
When you register custom attributes or elements with Q3ElementClass_Register, the name you use doesn't have to be the exact same name used by other developers for that type. As an example, the W3Anchor type is defined as 'wwwa' and its name is "W3Anchor." An Apple implementation of this attribute might be registered as Q3ElementClass_Register('wwwa', "Apple:W3Anchor"), and a third party's implementation might be registered as Q3ElementClass_Register('wwwa', "Microspot:W3Anchor"). The name is unimportant; because both of the implementations have the same type, data written by one will, if the implementation of both is the same, be read by the other.

DEFINING OUR METAHANDLER

Whenever QuickDraw 3D needs to operate on the data encapsulated by our custom element, it will call our metahandler, which we supplied a pointer to in the registration routine. Our metahandler (Listing 4) returns the addresses of the methods associated with our element type. We supply object I/O methods to preserve our element during I/O, and copy methods to allocate and manage the string memory. We return NULL by default, to indicate that unknown methods aren't supported and that a default method should be used. The definition for each of the routines is on this issue's CD.

Listing 4. The metahandler for our custom element

static TQ3FunctionPointer W3Anchor_MetaHandler(TQ3MethodType methodType)
{
   switch (methodType) {
      case kQ3MethodTypeObjectTraverse:
         return (TQ3FunctionPointer) W3Anchor_Traverse;
      case kQ3MethodTypeObjectWrite:
         return (TQ3FunctionPointer) W3Anchor_Write;
      case kQ3MethodTypeObjectReadData:
         return (TQ3FunctionPointer) W3Anchor_ReadData;
      case kQ3MethodTypeElementCopyAdd:
      case kQ3MethodTypeElementCopyGet:
      case kQ3MethodTypeElementCopyDuplicate:
         return (TQ3FunctionPointer) W3Anchor_CopyAdd;
      case kQ3MethodTypeElementCopyReplace:
         return (TQ3FunctionPointer) W3Anchor_CopyReplace;
      case kQ3MethodTypeElementDelete:
         return (TQ3FunctionPointer) W3Anchor_Delete;
      default:
         return (TQ3FunctionPointer) NULL;
   }
}

IMPLEMENTING THE METHODS

Listing 5 shows how the three element methods -- W3Anchor_CopyAdd, W3Anchor_CopyReplace, and W3Anchor_Delete -- are implemented. Note in Listing 4 that the same function, W3Anchor_CopyAdd, is used for the CopyAdd, CopyGet, and CopyDuplicate methods. This means that the data pointer passed into Q3Set_Add and Q3Set_Get is a pointer to the same structure as the internal structure. If you want to see how the I/O methods are implemented, look at the source code for our custom element on the CD.

Listing 5. Implementing the element methods

/* W3Anchor_CopyAdd adds the WWW data from src to dst. */
static TQ3Status W3Anchor_CopyAdd(W3AnchorData *src, W3AnchorData *dst)
{
   long   i;
   
   /* Check to see if src is a valid W3Anchor. */
   if (src->url == NULL)
      return kQ3Failure;

   /* We need to allocate memory for the string that belongs to */
   /* dst. */
   i = strlen(src->url);
   if (i == 0)
      return kQ3Failure;
   dst->url = (char *) malloc(i + 1);
   if (dst->url == NULL)
      return kQ3Failure;

   /* Copy the string from src to dst. */
   strcpy(dst->url, src->url);

   /* Check to see if src had a description. */
   if (src->description) {
      TQ3StringObject   stringReference;
      /* Get a reference to src's description object. */
      stringReference = Q3Shared_GetReference(src->description);
      if (stringReference == NULL)
         return kQ3Failure;
      dst->description = stringReference;
   } else
      dst->description = NULL;

   /* Just copy the options, since they're just values. */
   dst->options = src->options;
   return kQ3Success;
}
/* W3Anchor_CopyReplace substitutes the WWW data in src for the data 
   in dst. */
static TQ3Status W3Anchor_CopyReplace(W3AnchorData *src, 
                                       W3AnchorData *dst)
{
   long   i;
   char   *c;
   
   /* Check to see if src is a valid W3Anchor. */
   if (src->url == NULL)
      return kQ3Failure;

   /* We need to have enough memory for the string from src. */
   i = strlen(src->url);
   if (i == 0)
      return kQ3Failure;
   c = (char *) realloc(dst->url, i + 1);
   if (c == NULL)
      return kQ3Failure;

   dst->url = c;
   strcpy(dst->url, src->url);
   if (src->description) {
      TQ3StringObject   stringReference;

      /* Get a reference to src's description object. */
      stringReference = Q3Shared_GetReference(src->description);

      if (stringReference == NULL)
         return kQ3Failure;
      if (dst->description)
         Q3Object_Dispose(dst->description);
      dst->description = stringReference;
   } else
      dst->description = NULL;
   dst->options = src->options;
   return kQ3Success;
}

/* W3Anchor_Delete cleans up the references and memory allocations. */
static TQ3Status W3Anchor_Delete(W3AnchorData *myURLData)
{
   if (myURLData->url != NULL) {
      free(myURLData->url);
      myURLData->url = NULL;
   }
   if (myURLData->description != NULL) {
      Q3Object_Dispose(myURLData->description);
      myURLData->description = NULL;
   }
   return kQ3Success;
}

ATTACHING THE CUSTOM ELEMENT

Once we've set up our metahandler and associated routines, we can use the normal set and attribute set routines to add elements and attributes of our custom type. Listing 6 shows how we add our custom element to a set and attach the set to a shape object. Our geometric object will then inherit the set from the shape.

Listing 6. Adding our custom element to a set and attaching the set to an object

TQ3AttributeSet   theSet;
W3AnchorData      QD3DHomePage;

theSet = Q3Set_New();
if (theSet) {
   char   *description = "Apple QuickDraw 3D Home Page",
         *url = "http://www.info.apple.com/qd3d";
   QD3DHomePage.url = malloc(strlen(url) + 1);
   if (QD3DHomePage.url) {
      strcpy(url, QD3DHomePage.url);
      QD3DHomePage.description = Q3CString_New(description);
      QD3DHomePage.options = 0;

      /* Add the anchor data to the set. */
      Q3Set_Add(theSet, kElementTypeW3Anchor, &QD3DHomePage);

      /* The data has been copied and objects referenced, so we need
         to clean up after ourselves. */
      free(QD3DHomePage.url);
      Q3Object_Dispose(QD3DHomePage.description);
   }

   /* Attach the set to a shape. */
   Q3Shape_SetSet(aShape, theSet);
   Q3Object_Dispose(theSet);
} 

GETTING THE CUSTOM DATA FROM THE OBJECT

At some point your application will want to extract the custom data you've attached to an object. In our sample application, that point is reached when the user clicks on an object or the cursor passes over an object. The W3Anchor_GetFromObject routine (Listing 7) gets custom data from an object passed into the routine, using the QuickDraw 3D routines Q3Set_Contains and Q3Set_Get.

Listing 7. Getting our custom data from the object

TQ3Boolean W3Anchor_GetFromObject(TQ3Object object, W3AnchorData *data)
{
   TQ3SetObject   set;
   TQ3Boolean      result;
   
   W3Anchor_Empty(data);
   data->url = NULL;
   data->description = NULL;
   set = NULL;

   /* The object passed in must be a shape or a geometry. */
   if (Q3Object_IsType(object, kQ3ShapeTypeGeometry) == kQ3True) {
      Q3Geometry_GetAttributeSet(object, &set);
      if (set != NULL) {
         result = W3Anchor_GetFromSet(set, data);
         Q3Object_Dispose(set);
         if (result == kQ3True)
            return result;
         set = NULL;
      }
   }
   if (Q3Object_IsType(object, kQ3SharedTypeShape) == kQ3True) {
      Q3Shape_GetSet(object, &set);
      if (set != NULL) {
         result = W3Anchor_GetFromSet(set, data);
         Q3Object_Dispose(set);
         return result;
      }
   }
   return kQ3False;
}

TQ3Boolean W3Anchor_GetFromSet(TQ3SetObject set, W3AnchorData *data)
{
   TQ3Object         unkObj;
   TQ3Boolean         result;
   TQ3GroupPosition   position;

   result = kQ3False;

   /* Ideally, you'll find one of these. */
   if (Q3Set_Contains(set, kElementTypeW3Anchor) == kQ3True) {
      if (Q3Set_Get(set, kElementTypeW3Anchor, data) == kQ3Failure)
         return kQ3False;   /* Error: Contains, but can't get! */
      return kQ3True;
   }

   /* But due to a bug in QuickDraw 3D versions prior to 1.0.4, the
      element may be contained within another set in the unknown 
      element. */
   if (Q3Set_Contains(set, kQ3ElementTypeUnknown) == kQ3True) {
      if (Q3Set_Get(set, kQ3ElementTypeUnknown, &unkObj) ==
            kQ3Failure)
         return kQ3False;   /* Error: Contains, but can't get! */
      if (unkObj == NULL)
         return kQ3False;
      /* Unknown objects may contain one object or a group. */
      if (Q3Object_IsType(unkObj, kQ3SharedTypeSet) == kQ3True)
         result = W3Anchor_GetFromSet(unkObj, data);
      else if (Q3Object_IsType(unkObj, kQ3ShapeTypeGroup) ==
             kQ3True) {
         Q3Group_GetFirstPositionOfType(unkObj, kQ3SharedTypeSet,
            &position);
         if (position != NULL) {
            Q3Group_GetPositionObject(unkObj, position, &set);
            result = W3Anchor_GetFromSet(set, data);
         }
      }
      Q3Object_Dispose(unkObj);
   }
   return result;
}
W3Anchor_GetFromObject includes a workaround for an interesting problem. In QuickDraw 3D before version 1.0.4, if an element or attribute type was unknown (in other words, if a metahandler wasn't installed for the element or attribute), the element or attribute would be read as an unknown object. When a set was defined as part of an object derived from the Shape class, the set was written out to the metafile just fine; but when the set was read from the metafile into the shape's set, it was read as an unknown object, resulting in an additional, unnecessary level of containment, as illustrated in Figure 3. If you're reading custom elements or attributes from 3DMF files, you need to ensure that your users have version 1.0.4 or later, or you'll need to work around this issue.

Figure 3. The problem with reading a set from a metafile

Similarly, if a metafile containing an object with a custom element attached to it is read by an application that doesn't know about that custom element, when the object is written out its associated custom element will be written out as an unknown object. The moral of this story is that you should check inside an unknown object to see if the type of attribute it contains is the one you're looking for.

SENDING THE URL TO A BROWSER

Once we've extracted the URL from our custom element, we want to send it to Netscape Navigatoror a similar browser. Listing 8 shows the basics of how to do this (we've left out the proper error handling in the interest of saving space).
    A more complete example on this issue's CD shows how to detect whether Netscape Navigator is running and, if not, to launch the application. It shows other cool uses for custom elements and attributes as well.*

Listing 8. Sending the URL to a browser

Boolean OpenURL(char *name)
{
   AppleEvent   theAppleEvent, theReply;
   OSErr         err;

   /* If Netscape isn't around, get out. */
   if (Find_Netscape() == false)
      return false;

   /* Netscape is here; let's send them an Apple event. */
   err = AECreateAppleEvent('WWW!', 'OURL', &theAddressDesc,
            kAutoGenerateReturnID, kAnyTransactionID, &theAppleEvent);
   err = AEPutParamPtr(&theAppleEvent, keyDirectObject, typeChar,
            name, strlen(name));
   err = AESend(&theAppleEvent, &theReply, kAEWaitReply, 
            kAENormalPriority, kNoTimeOut, NewAEIdleProc(MyIdle),
            NewAEFilterProc(MyFilter));
   if (err == noErr)
      return true;
   else
      return false;   
}

NEW CUSTOM ELEMENTS AND ATTRIBUTES

In future releases of QuickDraw 3D, you'll be able to ship your custom elements and attributes as a shared library that plugs into QuickDraw 3D, as opposed to having to compile the code within your application. This will allow for the custom elements and attributes to be valid for all QuickDraw 3D applications running on the machine.

In the meantime, for your custom element or attribute to be shared and understood by other applications, you can propose it to the Developer Support Center (devsupport@applelink.apple.com or AppleLink DEVSUPPORT), and they'll pass the information on to the QuickDraw 3D team. Be sure to specify the data format, describe how the object is to be used, and include a C-based implementation. We want to avoid the problems experienced with QuickDraw's picture comments, where the behavior or meaning of the data was often not clear. If enough developers request similar attributes or elements, we'll add them to the next release of QuickDraw 3D, so that they get registered at startup time. In any case, we'll make the specifications for custom attributes and elements available on the Developer CD Series, the develop Bookmark CD, and the Web (http://www.info.apple.com/qd3d/).

Following are descriptions of six new custom elements that address needs expressed by several of our developers. Implementations for these are provided on the CD, in the file CustomAttribute_Lib.c. (Yes, these are elements, even though we on the QuickDraw 3D team have been in the sloppy habit of referring to them as attributes, and both the filename and the names of some of the elements reflect that habit. Just make sure that you're more precise in your use of the terms element and attribute, now that you know what the difference is from reading this article.) In addition, the CD contains a Technote describing some custom elements and attributes defined by our developer community.

NAMEATTRIBUTE

This element contains a string object. It can be attached to any object in the Shape class or any subclass of the Shape class. It can also be added to an attribute set and assigned to a geometry or faces. (In future releases we'll have other subclasses to the String class, allowing you to use non-ASCII characters.)

Written out in a 3DMF text metafile, this element appears as follows:

Container (
   NameAttribute ( )
   CString ( "1 meter box" )
)

SCALEATTRIBUTE

This element, of type double, determines the relation between one unit in the model and one meter. For example, if one unit in your model is equivalent to 10 meters, the scale should be set to 10.

This element, and all the elements whose descriptions follow, should be attached only to groups or geometry objects. Also, for each of these elements, traversal to find the element should be top down; this means that if it's attached to a group, there's no need to traverse the objects within the group.

If you add objects that have a scale element to a group, make sure that the objects are transformed (placed in a group with a transform and then added to the main group) so that the scale for the group is uniform.

ScaleAttribute ( 1.0 )

UPVECTOR AND FORWARDDIRECTION

These elements, of type TQ3Vector3D, specify the up vector and forward direction for a model. They're used to ensure that the orientation of an object read from a metafile is correct (that is, that it has the right side up and faces the right way). As for ScaleAttribute, if you add objects that have either of these elements to a group, make sure that the objects are transformed.
UpVector ( 0.0 1.0 0.0 )
ForwardDirection ( 1.0 0.0 0.5 )

W3ANCHOR

This element contains a URL in the form of a C string (ASCII only), an option field, which can be set to kURLReferenceOptionUseMap (meaning that the application should attach a pointer to the URL before sending the URL to the server), and a string object to encapsulate the description of the site pointed to by the URL (note that this allows for non-ASCII descriptions in the future). In the following 3DMF text, the "0" signifies that there's no map. "Apple's home page" is shown as a CString when it's actually a TQ3StringObject, because that's how we decided to represent strings in the metafile.
Container (
   W3Anchor ( "http://www.info.apple.com" 0 )
   CString ( "Apple's home page" )
)

W3INLINE

This element contains a URL in the form of an C string (ASCII only). The group or geometry that this element is attached to acts as a proxy for the data pointed to by the URL. This allows the application to perform the URL data retrieval on a separate thread or in the background, or delay the operation until the user expresses interest in the proxy. Once the URL data is retrieved, the data should replace the object that holds the element.
W3Inline ( "http:www.info.apple.com" )

CUSTOMIZING YOUR WORLD

This article has given an overview of what you can do with custom elements and attributes. With your imagination and a few simple routines, you can extend the rich capabilities of QuickDraw 3D. Have fun with custom elements and attributes, and don't forget to tell us about them if you want other developers to be able to use yours!

    RELATED READING

    • "QuickDraw 3D: A New Dimension for Macintosh Graphics" by Pablo Fernicola and Nick Thompson, develop Issue 22, and "The Basics of QuickDraw 3D Geometries" by Nick Thompson and Pablo Fernicola, develop Issue 23.

    • 3D Graphics Programming With QuickDraw 3D (Addison-Wesley, 1995).

    • http://www.info.apple.com/qd3d -- the QuickDraw 3D home page, containing links to sites with inline 3DMF data.


NICK THOMPSON (nickt@applelink.apple.com, AppleLink NICKT) is still looking for new ways to make the rest of the QuickDraw 3D team members think about Mac OS 8. Nick is working on integrating the award-winning QuickDraw 3D software into Mac OS 8, taking maximum advantage of the modern OS features. When not immersed in the future of QuickDraw 3D, this former Developer Technical Support engineer is immersed in water, surfing off the Northern California coast.*

PABLO FERNICOLA (EscherDude@aol.com, AppleLink PFF) has been really busy since you last heard from him. At MACWORLD San Francisco in January he was busy explaining to developers and users what QuickDraw 3D means to them. As we write this, he's busy planning the next three releases of QuickDraw 3D with the team. As technical lead of the QuickDraw 3D team, he misses life without meetings, but is totally stoked that the team has won awards from Byte, Macworld, and MacUser.*

KENT DAVIDSON (dbunny@apple.com, AppleLink DBUNNY), a.k.a. 3DMF Dude, Object Dude, and "The Man" (by the dudes in Marketing), is the guy who keeps the core of QuickDraw 3D humming. Outside of commuting from San Francisco to the 'burbs of Cupertino, he spends his time rock climbing, skiing, and hanging out. He's currently wracking his brain over the plug-in renderer architecture, which he'll finish as soon as everyone leaves him alone.*

Thanks to our technical reviewers Rick Evans, Robin Landsbert, Philip McBride, Klaus Strelau, and David Vasquez, and also to the entire QuickDraw 3D team.*

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Dropbox 193.4.5594 - Cloud backup and sy...
Dropbox is a file hosting service that provides cloud storage, file synchronization, personal cloud, and client software. It is a modern workspace that allows you to get to all of your files, manage... Read more
Google Chrome 122.0.6261.57 - Modern and...
Google Chrome is a Web browser by Google, created to be a modern platform for Web pages and applications. It utilizes very fast loading of Web pages and has a V8 engine, which is a custom built... Read more
Skype 8.113.0.210 - Voice-over-internet...
Skype is a telecommunications app that provides HD video calls, instant messaging, calling to any phone number or landline, and Skype for Business for productive cooperation on the projects. This... Read more
Tor Browser 13.0.10 - Anonymize Web brow...
Using Tor Browser you can protect yourself against tracking, surveillance, and censorship. Tor was originally designed, implemented, and deployed as a third-generation onion-routing project of the U.... Read more
Deeper 3.0.4 - Enable hidden features in...
Deeper is a personalization utility for macOS which allows you to enable and disable the hidden functions of the Finder, Dock, QuickTime, Safari, iTunes, login window, Spotlight, and many of Apple's... Read more
OnyX 4.5.5 - Maintenance and optimizatio...
OnyX is a multifunction utility that you can use to verify the startup disk and the structure of its system files, to run miscellaneous maintenance and cleaning tasks, to configure parameters in the... Read more

Latest Forum Discussions

See All

Zenless Zone Zero opens entries for its...
miHoYo, aka HoYoverse, has become such a big name in mobile gaming that it's hard to believe that arguably their flagship title, Genshin Impact, is only three and a half years old. Now, they continue the road to the next title in their world, with... | Read more »
Live, Playdate, Live! – The TouchArcade...
In this week’s episode of The TouchArcade Show we kick things off by talking about all the games I splurged on during the recent Playdate Catalog one-year anniversary sale, including the new Lucas Pope jam Mars After Midnight. We haven’t played any... | Read more »
TouchArcade Game of the Week: ‘Vroomies’
So here’s a thing: Vroomies from developer Alex Taber aka Unordered Games is the Game of the Week! Except… Vroomies came out an entire month ago. It wasn’t on my radar until this week, which is why I included it in our weekly new games round-up, but... | Read more »
SwitchArcade Round-Up: ‘MLB The Show 24’...
Hello gentle readers, and welcome to the SwitchArcade Round-Up for March 15th, 2024. We’re closing out the week with a bunch of new games, with Sony’s baseball franchise MLB The Show up to bat yet again. There are several other interesting games to... | Read more »
Steam Deck Weekly: WWE 2K24 and Summerho...
Welcome to this week’s edition of the Steam Deck Weekly. The busy season has begun with games we’ve been looking forward to playing including Dragon’s Dogma 2, Horizon Forbidden West Complete Edition, and also console exclusives like Rise of the... | Read more »
Steam Spring Sale 2024 – The 10 Best Ste...
The Steam Spring Sale 2024 began last night, and while it isn’t as big of a deal as say the Steam Winter Sale, you may as well take advantage of it to save money on some games you were planning to buy. I obviously recommend checking out your own... | Read more »
New ‘SaGa Emerald Beyond’ Gameplay Showc...
Last month, Square Enix posted a Let’s Play video featuring SaGa Localization Director Neil Broadley who showcased the worlds, companions, and more from the upcoming and highly-anticipated RPG SaGa Emerald Beyond. | Read more »
Choose Your Side in the Latest ‘Marvel S...
Last month, Marvel Snap (Free) held its very first “imbalance" event in honor of Valentine’s Day. For a limited time, certain well-known couples were given special boosts when conditions were right. It must have gone over well, because we’ve got a... | Read more »
Warframe welcomes the arrival of a new s...
As a Warframe player one of the best things about it launching on iOS, despite it being arguably the best way to play the game if you have a controller, is that I can now be paid to talk about it. To whit, we are gearing up to receive the first... | Read more »
Apple Arcade Weekly Round-Up: Updates an...
Following the new releases earlier in the month and April 2024’s games being revealed by Apple, this week has seen some notable game updates and events go live for Apple Arcade. What The Golf? has an April Fool’s Day celebration event going live “... | Read more »

Price Scanner via MacPrices.net

Apple Education is offering $100 discounts on...
If you’re a student, teacher, or staff member at any educational institution, you can use your .edu email address when ordering at Apple Education to take $100 off the price of a new M3 MacBook Air.... Read more
Apple Watch Ultra 2 with Blood Oxygen feature...
Best Buy is offering Apple Watch Ultra 2 models for $50 off MSRP on their online store this week. Sale prices available for online orders only, in-store prices may vary. Order online, and choose... Read more
New promo at Sams Club: Apple HomePods for $2...
Sams Club has Apple HomePods on sale for $259 through March 31, 2024. Their price is $40 off Apple’s MSRP, and both Space Gray and White colors are available. Sale price is for online orders only, in... Read more
Get Apple’s 2nd generation Apple Pencil for $...
Apple’s Pencil (2nd generation) works with the 12″ iPad Pro (3rd, 4th, 5th, and 6th generation), 11″ iPad Pro (1st, 2nd, 3rd, and 4th generation), iPad Air (4th and 5th generation), and iPad mini (... Read more
10th generation Apple iPads on sale for $100...
Best Buy has Apple’s 10th-generation WiFi iPads back on sale for $100 off MSRP on their online store, starting at only $349. With the discount, Best Buy’s prices are the lowest currently available... Read more
iPad Airs on sale again starting at $449 on B...
Best Buy has 10.9″ M1 WiFi iPad Airs on record-low sale prices again for $150 off Apple’s MSRP, starting at $449. Sale prices for online orders only, in-store price may vary. Order online, and choose... Read more
Best Buy is blowing out clearance 13-inch M1...
Best Buy is blowing out clearance Apple 13″ M1 MacBook Airs this weekend for only $649.99, or $350 off Apple’s original MSRP. Sale prices for online orders only, in-store prices may vary. Order... Read more
Low price alert! You can now get a 13-inch M1...
Walmart has, for the first time, begun offering new Apple MacBooks for sale on their online store, albeit clearance previous-generation models. They now have the 13″ M1 MacBook Air (8GB RAM, 256GB... Read more
Best Apple MacBook deal this weekend: Get the...
Apple has 13″ M2 MacBook Airs available for only $849 today in their Certified Refurbished store. These are the cheapest M2-powered MacBooks for sale at Apple. Apple’s one-year warranty is included,... Read more
New 15-inch M3 MacBook Air (Midnight) on sale...
Amazon has the new 15″ M3 MacBook Air (8GB RAM/256GB SSD/Midnight) in stock and on sale today for $1249.99 including free shipping. Their price is $50 off MSRP, and it’s the lowest price currently... Read more

Jobs Board

Early Preschool Teacher - Glenda Drive/ *Appl...
Early Preschool Teacher - Glenda Drive/ Apple ValleyTeacher Share by Email Share on LinkedIn Share on Twitter Read more
Senior Software Engineer - *Apple* Fundamen...
…center of Microsoft's efforts to empower our users to do more. The Apple Fundamentals team focused on defining and improving the end-to-end developer experience in Read more
Relationship Banker *Apple* Valley Main - W...
…Alcohol Policy to learn more. **Company:** WELLS FARGO BANK **Req Number:** R-350696 **Updated:** Mon Mar 11 00:00:00 UTC 2024 **Location:** APPLE VALLEY,California Read more
Medical Assistant - Surgical Oncology- *Apple...
Medical Assistant - Surgical Oncology- Apple Hill WellSpan Medical Group, York, PA | Nursing | Nursing Support | FTE: 1 | Regular | Tracking Code: 200555 Apply Now Read more
Early Preschool Teacher - Glenda Drive/ *Appl...
Early Preschool Teacher - Glenda Drive/ Apple ValleyTeacher Share by Email Share on LinkedIn Share on Twitter Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.