Home My Page Projects Code Snippets Project Openings 3D graphics for Standard ML
Summary Activity SCM

SCM Repository

[sml3d] Annotation of /trunk/sml3d/src/particles/particles.sml
ViewVC logotype

Annotation of /trunk/sml3d/src/particles/particles.sml

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1093 - (view) (download)

1 : jhr 1 (* particles.sml
2 :     *
3 : jhr 1050 * COPYRIGHT (c) 2011 John Reppy (http://www.cs.uchicago.edu/~jhr)
4 : jhr 1 * All rights reserved.
5 :     *)
6 : pavelk 476
7 : jhr 548 structure ParticlesImp =
8 : jhr 1 struct
9 :    
10 : pavelk 672 structure OP = OldParticles
11 : pavelk 547 structure PSV = PSVar
12 : pavelk 687 structure IR = PSysIR
13 : jhr 548 structure VSet = PSV.Set
14 : pavelk 672 structure DB = DataBuffer
15 :    
16 : jhr 1 type float = GL.float
17 : jhr 583 type vec3f = Vec3f.vec3
18 : pavelk 429 type color4f = SML3dTypes.color4f
19 :     type color3f = SML3dTypes.color3f
20 : pavelk 672
21 : pavelk 769 fun printErr s = TextIO.output(TextIO.stdErr, s ^ "\n")
22 : jhr 1
23 : pavelk 672 (***** DOMAINS *****)
24 :    
25 : pavelk 969 type 'a var = 'a PSV.var
26 : pavelk 1092 type 'a state_var = 'a PSV.state_var
27 :    
28 :     val sv_pos = PSV.newSV("sv_pos", PSV.vec3fTy)
29 :     val sv_vel = PSV.newSV("sv_vel", PSV.vec3fTy)
30 :    
31 : pavelk 1008 datatype 'a domain
32 :     = D_POINT of 'a var
33 :     | D_LINE of {pt1: 'a var, pt2: 'a var}
34 :     | D_TRIANGLE of {pt1: 'a var, pt2: 'a var, pt3: 'a var}
35 :     | D_PLANE of {pt: 'a var, normal: 'a var}
36 :     | D_RECT of {pt: 'a var, htvec: 'a var, wdvec: 'a var}
37 :     | D_BOX of {min: 'a var, max: 'a var}
38 :     | D_SPHERE of {center: 'a var, irad: 'a var, orad: 'a var}
39 :     | D_CYLINDER of {pt1: 'a var, pt2: 'a var, irad: float var, orad: float var}
40 :     | D_CONE of {pt1: 'a var, pt2: 'a var, irad: float var, orad: float var}
41 :     | D_BLOB of {center: 'a var, stddev: float var}
42 :     | D_DISC of {pt: 'a var, normal: 'a var, irad: float var, orad: float var}
43 : pavelk 1093
44 :    
45 :     datatype 'a expr
46 :     = CONST of 'a
47 :     | VAR of 'a var
48 :     | STATE_VAR of 'a state_var
49 :     | GENERATE of 'a domain
50 :     | ADD of 'a expr * 'a expr
51 :     | SCALE of 'a expr * 'b expr
52 :     | NEG of 'a expr
53 :     | TEST_WITHIN of {var : 'b expr, d : 'b domain, thenExpr : 'a expr, elseExpr : 'a expr}
54 :     (* ... *)
55 :     | END
56 :    
57 :     fun varToExp(v) = VAR v
58 :     fun stateVarToExp(v) = STATE_VAR v
59 :     fun const(v) = CONST v
60 :     fun generate(d) = GENERATE d
61 :    
62 :     fun add(e1, e2) = ADD (e1, e2)
63 :     fun scale(scalar, vector) = SCALE (vector, scalar)
64 :     fun neg(exp) = NEG exp
65 :    
66 :     fun testWithin(exp) = TEST_WITHIN(exp)
67 :    
68 : pavelk 547
69 :     (* get the free variables of a domain *)
70 : jhr 548 fun fvsOfDomain (D_POINT x) = VSet.singleton x
71 :     | fvsOfDomain (D_LINE{pt1, pt2}) = VSet.fromList([pt1, pt2])
72 :     | fvsOfDomain (D_TRIANGLE{pt1, pt2, pt3}) = VSet.fromList([pt1, pt2, pt3])
73 :     | fvsOfDomain (D_PLANE{pt, normal}) = VSet.fromList([pt, normal])
74 :     | fvsOfDomain (D_RECT{pt, htvec, wdvec}) = VSet.fromList([pt, htvec, wdvec])
75 :     | fvsOfDomain (D_BOX{min, max}) = VSet.fromList([min, max])
76 :     | fvsOfDomain (D_SPHERE{center, irad, orad}) = VSet.fromList([center, irad, orad])
77 :     | fvsOfDomain (D_CYLINDER{pt1, pt2, irad, orad}) = VSet.fromList([pt1, pt2, irad, orad])
78 :     | fvsOfDomain (D_CONE{pt1, pt2, irad, orad}) = VSet.fromList([pt1, pt2, irad, orad])
79 :     | fvsOfDomain (D_BLOB{center, stddev}) = VSet.fromList([center, stddev])
80 :     | fvsOfDomain (D_DISC{pt, normal, irad, orad}) = VSet.fromList([pt, normal, irad, orad])
81 :    
82 :     val point = D_POINT
83 : jhr 974 fun line (endPt1, endPt2) = D_LINE{pt1 = endPt1, pt2 = endPt2}
84 :     fun triangle (p1, p2, p3) = D_TRIANGLE{pt1 = p1, pt2 = p2, pt3 = p3}
85 : pavelk 771 fun plane {pt, n} = D_PLANE{pt=pt, normal=n}
86 :     fun rectangle {corner, wid, ht} = D_RECT{pt=corner, htvec=ht, wdvec=wid}
87 :     fun box {min, max} = D_BOX{min=min, max=max}
88 :     fun sphere {c, r} = D_SPHERE{center = c, irad = PSV.constf 0.0, orad = r}
89 :     fun ball {c, ir, or} = D_SPHERE{center=c, irad=ir, orad=or}
90 :     fun tube {point1, point2, irad, orad} =
91 :     D_CYLINDER{pt1=point1, pt2=point2, irad=irad, orad=orad}
92 :     fun cylinder {point1, point2, rad} =
93 :     D_CYLINDER{pt1=point1, pt2=point2, irad= PSV.constf 0.0, orad = rad}
94 :     fun cone {apex, cbase, irad, orad} =
95 :     D_CONE{pt1=apex, pt2=cbase, irad=irad, orad=orad}
96 :     fun blob {center, stddev} = D_BLOB{center=center, stddev=stddev}
97 :     fun disc {center, normal, irad, orad} =
98 :     D_DISC{pt=center, normal=normal, irad=irad, orad=orad}
99 : pavelk 1008 fun range r = box r
100 : pavelk 1017 val value = point
101 : jhr 548
102 :    
103 : pavelk 547 (***** ACTIONS *****)
104 : jhr 1
105 : pavelk 547 datatype act
106 :     (* Particles are tested to see whether they will pass from being outside the specified domain to
107 :     * being inside it within look_ahead time units from now if the next Move() action were to occur
108 :     * now. The specific direction and amount of turn is dependent on the kind of domain being
109 :     * avoided.
110 :     *)
111 : pavelk 1008 = AVOID of vec3f domain
112 :     (* Particles are tested to see whether they will pass from being outside the specified vec3f domain to
113 : jhr 548 * being inside it if the next Move() action were to occur now. If they would pass through the
114 : pavelk 1008 * surface of the vec3f domain, they are instead bounced off it. That is, their velocity vector is
115 : jhr 548 * decomposed into components normal to the surface and tangent to the surface. The direction of
116 :     * the normal component is reversed, and friction, resilience and cutoff are applied to the
117 :     * components. They are then recomposed into a new velocity heading away from the surface.
118 :     *)
119 : pavelk 1008 | BOUNCE of {friction : float PSV.var, resilience : float PSV.var, cutoff : float PSV.var, d : vec3f domain}
120 : jhr 548 (* If a particle's velocity magnitude is within vlow and vhigh, then multiply each component of
121 :     * the velocity by the respective damping constant. Typically, the three components of damping
122 :     * will have the same value.
123 :     *)
124 :     | DAMPING
125 : pavelk 547 (* Causes an explosion by accelerating all particles away from the center. Particles are
126 :     * accelerated away from the center by an amount proportional to magnitude. The shock wave of
127 :     * the explosion has a gaussian magnitude. The peak of the wave front travels spherically
128 :     * outward from the center at the specified velocity. So at a given time step, particles at a
129 :     * distance (velocity * age) from center will receive the most acceleration, and particles not
130 :     * at the peak of the shock wave will receive a lesser outward acceleration.
131 :     *)
132 : jhr 548 | EXPLOSION
133 : pavelk 547 (* This allows snaky effects where the particles follow each other. Each particle is accelerated
134 :     * toward the next particle in the group. The follow action does not affect the last particle in
135 :     * the group. This allows controlled effects where the last particle in the group is killed
136 :     * after each time step and replaced by a new particle at a slightly different position. See
137 :     * KillOld to learn how to kill the last particle in the group after each step.
138 :     *)
139 : jhr 548 | FOLLOW
140 :     (* Each particle is accelerated toward each other particle. *)
141 :     | GRAVITATE
142 : jhr 974 (* The scaled acceleration vector is simply added to the velocity vector of each particle at
143 :     * each time step.
144 : pavelk 547 *)
145 : jhr 974 | ACCEL of float PSV.var
146 : pavelk 1008 (* For each particle within the jet's vec3f domain of influence, dom, Jet chooses an acceleration
147 :     * vector from the vec3f domain acc and applies it to the particle's velocity.
148 : pavelk 547 *)
149 : jhr 548 | JET
150 :     (* Removes all particles older than age_limit. But if kill_less_than is true, it instead
151 :     * removes all particles newer than age_limit. age_limit is not clamped, so negative values are
152 :     * ok. This can be used in conjunction with StartingAge(-n) to create and then kill a particular
153 :     * set of particles.
154 : pavelk 547 *)
155 : jhr 548 | KILLOLD
156 : pavelk 547 (* Each particle is accelerated toward the weighted mean of the velocities of the other
157 :     * particles in the group.
158 :     *)
159 : jhr 548 | MATCHVEL
160 : pavelk 547 (* This action actually updates the particle positions by adding the current velocity to the
161 :     * current position and the current rotational velocity to the current up vector. This is
162 :     * typically the last particle action performed in an iteration of a particle simulation, and
163 :     * typically only occurs once per iteration.
164 :     *)
165 : jhr 548 | MOVE
166 : pavelk 547 (* For each particle, this action computes the vector to the closest point on the line, and
167 :     * accelerates the particle in that direction.
168 :     *)
169 : pavelk 687 | ORBITLINESEG of {endp1 : vec3f PSV.var, endp2 : vec3f PSV.var, mag : float PSV.var, maxRad : float PSV.var}
170 : jhr 548 (* For each particle, this action computes the vector to the center point, and accelerates the
171 :     * particle in the vector direction.
172 : pavelk 547 *)
173 : pavelk 1074 | ORBITPOINT of {center : vec3f PSV.var, mag : float PSV.var, maxRad : float PSV.var}
174 : pavelk 1008 (* For each particle, chooses an acceleration vector from the specified vec3f domain and adds it to
175 : jhr 548 * the particle's velocity. Reducing the time step, dt, will make a higher probability of being
176 :     * near the original velocity after unit time. Smaller dt approach a normal distribution of
177 :     * velocity vectors instead of a square wave distribution.
178 : pavelk 547 *)
179 : pavelk 1008 | RANDOMACCEL of vec3f domain
180 :     (* Chooses a displacement vector from the specified vec3f domain and adds it to the particle's
181 : pavelk 547 * position. Reducing the time step, dt, will make a higher probability of being near the
182 :     * original position after unit time. Smaller dt approach a normal distribution of particle
183 :     * positions instead of a square wave distribution.
184 :     *)
185 : pavelk 1008 | RANDOMDISP of vec3f domain
186 : pavelk 547 (* For each particle, sets the particle's velocity vector to a random vector in the specified
187 : pavelk 1008 * vec3f domain. This function is not affected by dt.
188 : pavelk 547 *)
189 : pavelk 1008 | RANDOMVEL of vec3f domain
190 : jhr 548 (* Computes each particle’s speed (the magnitude of its velocity vector) and if it is less than
191 :     * min_speed or greater than max_speed the velocity is scaled to within those bounds, while
192 :     * preserving the velocity vector’s direction.
193 :     *)
194 :     | SPEEDLIMIT
195 :     (* Modifies the color and alpha of each particle to be scale percent of the way closer to the
196 :     * specified color and alpha. scale is multiplied by dt before scaling the sizes. Thus, using
197 :     * smaller dt causes a slightly faster approach to the target color.
198 :     *)
199 :     | TARGETCOLOR
200 :     (* Modifies the size of each particle to be scale percent of the way closer to the specified
201 :     * size triple. This makes sizes grow asymptotically closer to the given size. scale is
202 :     * multiplied by dt before scaling the sizes. Thus, using smaller dt causes a slightly faster
203 :     * approach to the target size. The separate scales for each component allow only selected
204 :     * components to be scaled.
205 :     *)
206 :     | TARGETSIZE
207 :     (* Modifies the velocity of each particle to be scale percent of the way closer to the specified
208 :     * velocity. This makes velocities grow asymptotically closer to the given velocity. scale is
209 :     * multiplied by dt before scaling the velocities. Thus, using smaller dt causes a slightly
210 :     * faster approach to the target velocity.
211 :     *)
212 :     | TARGETVEL
213 :     (* The vortex is a complicated action to use, but when done correctly it makes particles fly
214 :     * around like in a tornado.
215 :     *)
216 :     | VORTEX
217 : pavelk 769
218 : pavelk 770 (* The particle just dies (is considered invalid) *)
219 :     | DIE
220 :    
221 : pavelk 769 datatype cond
222 :     (* Tests whether or not the particle is within the given domain. *)
223 : pavelk 1008 = WITHIN of vec3f domain
224 :     | WITHINVEL of vec3f domain
225 : pavelk 769 (* ... *)
226 :    
227 :     datatype combinator
228 :     = PRED of pred
229 :     | SEQ of act list
230 :    
231 :     and pred = PR of {
232 :     ifstmt : cond,
233 :     thenstmt : combinator list,
234 :     elsestmt : combinator list
235 :     }
236 : jhr 548
237 :     datatype action = PSAE of {
238 : pavelk 769 action : combinator list,
239 : pavelk 687 vars : VSet.set
240 : jhr 548 }
241 : pavelk 770
242 : jhr 974 val nop = PSAE{action=[], vars=VSet.empty}
243 : pavelk 770
244 : pavelk 1008 fun within {d : vec3f domain, thenStmt : action, elseStmt : action} = let
245 : pavelk 770 val PSAE{action=elseAct, vars=elseVars} = elseStmt
246 :     val PSAE{action=thenAct, vars=thenVars} = thenStmt
247 :     in
248 :     PSAE{
249 :     action=[PRED(PR{ifstmt = WITHIN(d), thenstmt = thenAct, elsestmt = elseAct})],
250 :     vars = VSet.union(VSet.union(fvsOfDomain(d), thenVars), elseVars)
251 :     }
252 :     end
253 : jhr 548
254 :     local
255 : pavelk 769 fun mkAct (fvs, acts) = PSAE{action=[SEQ(acts)], vars=fvs}
256 : jhr 548 in
257 :     fun avoid {
258 :     magnitude : float PSV.var, epsilon : float PSV.var,
259 : pavelk 1008 lookAhead : float PSV.var, d : vec3f domain
260 : jhr 548 } = mkAct(VSet.addList(fvsOfDomain(d), [magnitude, epsilon, lookAhead]), [AVOID d])
261 :     fun bounce {
262 :     friction : float PSV.var, resilience : float PSV.var,
263 : pavelk 1008 cutoff : float PSV.var, d : vec3f domain
264 : pavelk 672 } = mkAct(VSet.addList(fvsOfDomain(d), [friction, resilience, cutoff]),
265 : pavelk 575 [BOUNCE {friction = friction, resilience = resilience, cutoff = cutoff, d = d}]
266 :     )
267 : jhr 548 fun damping {coeff : vec3f PSV.var, vlow : float PSV.var, vhi : float PSV.var} =
268 :     mkAct(VSet.fromList([coeff, vlow, vhi]), [DAMPING])
269 :     fun explosion {
270 :     center : vec3f PSV.var, velocity : float PSV.var, magnitude : float PSV.var,
271 :     stdev : float PSV.var, epsilon : float PSV.var, age : float PSV.var
272 :     } = mkAct(VSet.fromList([center, velocity, magnitude, stdev, epsilon, age]), [EXPLOSION])
273 :     fun follow {
274 :     magnitude : float PSV.var, epsilon : float PSV.var, maxRadius : float PSV.var
275 :     } = mkAct(VSet.fromList([magnitude, epsilon, maxRadius]), [FOLLOW])
276 :     fun gravitate {mag : float PSV.var, epsilon : float PSV.var, maxRad : float PSV.var}
277 :     = mkAct(VSet.fromList([mag, epsilon, maxRad]), [GRAVITATE])
278 : jhr 974 fun accelerate a = mkAct(VSet.singleton a, [ACCEL a])
279 : jhr 548 fun jet {center : vec3f PSV.var, mag : float PSV.var, epsilon : float PSV.var, maxRad : float PSV.var}
280 :     = mkAct(VSet.fromList([center, mag, epsilon, maxRad]), [JET])
281 :     fun killOld {ageLimit : float PSV.var, kill_less_than : bool PSV.var} =
282 :     mkAct(VSet.fromList([ageLimit, kill_less_than]), [KILLOLD])
283 :     fun matchVelocity {mag : float PSV.var, epsilon : float PSV.var, maxRad : float PSV.var}
284 :     = mkAct(VSet.fromList([mag, epsilon, maxRad]), [MATCHVEL])
285 : pavelk 672 val move = mkAct(VSet.empty, [MOVE])
286 : pavelk 1074 fun orbitLineSegment (olsAct as {endp1 : vec3f PSV.var, endp2 : vec3f PSV.var, mag : float PSV.var, maxRad : float PSV.var}) =
287 :     mkAct(VSet.fromList([endp1, endp2, mag, maxRad]), [ORBITLINESEG olsAct])
288 :     fun orbitPoint (opAct as {center : vec3f PSV.var, mag : float PSV.var, maxRad : float PSV.var})
289 :     = mkAct(VSet.fromList([center, mag, maxRad]), [ORBITPOINT opAct])
290 : jhr 548 fun randomAccel d = mkAct(fvsOfDomain d, [RANDOMACCEL d])
291 :     fun randomDisplace d = mkAct(fvsOfDomain d, [RANDOMDISP d])
292 :     fun randomVelocity d = mkAct(fvsOfDomain d, [RANDOMVEL d])
293 :     fun speedLimit {min : float PSV.var, max : float PSV.var}
294 :     = mkAct(VSet.fromList([min, max]), [SPEEDLIMIT])
295 :     fun targetColor {color : vec3f PSV.var, alpha: float PSV.var, scale : float PSV.var}
296 :     = mkAct(VSet.fromList([color, alpha, scale]), [TARGETCOLOR])
297 :     fun targetSize {size : vec3f PSV.var, scale : vec3f PSV.var}
298 :     = mkAct(VSet.fromList([size, scale]), [TARGETSIZE])
299 :     fun targetVelocity (targetVel, scale)
300 :     = mkAct(VSet.fromList([targetVel, scale]), [TARGETVEL])
301 :     fun vortex {
302 :     center : vec3f PSV.var, axis : vec3f PSV.var, mag : float PSV.var,
303 :     epsilon : float PSV.var, maxRad : float PSV.var
304 :     } = mkAct(VSet.fromList([center, axis, mag, epsilon, maxRad]), [VORTEX])
305 : pavelk 770 val die = mkAct(VSet.empty, [DIE])
306 : pavelk 672
307 : jhr 548 (* This is a generic "add an action to a current particle group" function. Currently action lists
308 :     * need to be recreated before each update call, but eventually this will only be done in setup.
309 :     *)
310 :     fun catAction (PSAE{vars=fvs1, action=acts1}, PSAE{vars=fvs2, action=acts2}) =
311 : pavelk 769 PSAE{action=(acts1 @ acts2), vars=VSet.union(fvs1, fvs2)}
312 : jhr 548 end (* local *)
313 : pavelk 429
314 : jhr 974 fun sequence [] = nop
315 :     | sequence acts = let
316 :     fun actBuilder [] = raise Fail "Should never reach here."
317 :     | actBuilder [a] = a
318 :     | actBuilder (a :: acs) = catAction(a, actBuilder(acs))
319 :     fun condense [] = []
320 :     | condense (a :: acs) = let
321 :     fun condensePred (PR{ifstmt, thenstmt, elsestmt}) = PR{
322 :     ifstmt = ifstmt,
323 :     thenstmt = condense thenstmt,
324 :     elsestmt = condense elsestmt}
325 :     in
326 :     case a
327 :     of PRED p => PRED(condensePred p) :: condense(acs)
328 :     | SEQ(actions) => let
329 :     val condensed = condense(acs)
330 :     val identity = a :: condensed
331 :     in
332 :     case condensed
333 :     of [] => identity
334 :     | a1 :: rest => (case a1
335 :     of PRED(p) => identity
336 :     | SEQ(actions2) => SEQ(actions @ actions2) :: rest
337 :     (* end case *))
338 :     (* end case *)
339 :     end
340 :     (* end case *)
341 :     end
342 :     val PSAE{action, vars} = actBuilder(acts)
343 :     in
344 :     PSAE{action = condense(action), vars = vars}
345 :     end
346 :    
347 : pavelk 866 type renderer = PSysIR.renderer
348 :     val points = PSysIR.POINTS
349 : pavelk 935 fun texQuads(img) = PSysIR.TEXTURED_QUADS(img)
350 : pavelk 866
351 : pavelk 1092 datatype 'a distribution
352 : pavelk 1017 = DIST_NORMAL of {mu : float var, sigma : float var}
353 :     | DIST_DEC_LIN
354 :     | DIST_INC_LIN
355 :     | DIST_UNIFORM
356 : pavelk 1092 | DIST_TEXTURED of Texture.texture_id
357 : pavelk 1017
358 :     fun normal_dist settings = DIST_NORMAL(settings)
359 :     val dec_lin_dist = DIST_DEC_LIN
360 :     val inc_lin_dist = DIST_INC_LIN
361 :     val uniform_dist = DIST_UNIFORM
362 : pavelk 1092 val uniform_vec_dist = DIST_UNIFORM
363 :     fun textured_float_dist tex = DIST_TEXTURED tex
364 :     fun textured_vec_dist tex = DIST_TEXTURED tex
365 : pavelk 1017
366 :     fun fvsOfDistribution(DIST_NORMAL {mu, sigma}) = VSet.fromList [mu, sigma]
367 :     | fvsOfDistribution(DIST_DEC_LIN) = VSet.empty
368 :     | fvsOfDistribution(DIST_INC_LIN) = VSet.empty
369 : pavelk 1092 | fvsOfDistribution(DIST_UNIFORM) = VSet.empty
370 :     | fvsOfDistribution(DIST_TEXTURED _) = VSet.empty
371 :    
372 :     datatype 'a generator = GEN of {
373 :     var : 'a PSV.state_var,
374 :     d : 'a domain,
375 :     dist : 'a distribution
376 :     }
377 :    
378 :     fun create_generator gen = GEN gen
379 : pavelk 1017
380 : pavelk 770 datatype emitter = EMIT of {
381 : pavelk 1092 range : float domain * float distribution,
382 :     szDomain : float domain * float distribution,
383 : pavelk 1008 posDomain : vec3f domain,
384 :     velDomain : vec3f domain,
385 :     colDomain : vec3f domain,
386 : jhr 974 vars : VSet.set
387 :     }
388 : pavelk 672
389 : pavelk 1074 fun newEmitter({range as (rDom, rDist),
390 :     szRange as (szDom, szDist),
391 :     posDomain, velDomain, colDomain
392 :     }) = EMIT{
393 :     range = range,
394 :     szDomain = szRange,
395 :     posDomain = posDomain,
396 :     velDomain = velDomain,
397 :     colDomain = colDomain,
398 :     vars =
399 :     VSet.union(fvsOfDomain(rDom),
400 :     VSet.union(fvsOfDistribution(rDist),
401 :     VSet.union(fvsOfDomain(szDom),
402 :     VSet.union(fvsOfDistribution(szDist),
403 :     VSet.union(fvsOfDomain(posDomain),
404 :     VSet.union(fvsOfDomain(velDomain), fvsOfDomain(colDomain))
405 :     )))))
406 : jhr 1050 }
407 : pavelk 770
408 : jhr 1050 datatype program = PG of {
409 : jhr 974 emit: emitter,
410 :     act : action,
411 :     render : renderer
412 :     }
413 : pavelk 866
414 : jhr 1050 val create = PG
415 : pavelk 770
416 : jhr 974 end

root@smlnj-gforge.cs.uchicago.edu
ViewVC Help
Powered by ViewVC 1.0.0