# Copyright 2008 by Peter Moxhay and Wade Brainerd. # This file is part of Math. # # Math is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Math is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Math. If not, see . from objectarea import Object from vector import Vector from movableobject import MovableObject from faucetobject import FaucetObject import gtk, math class VolumeObject(MovableObject): """Quasi three-dimensional container object.""" FILL_RATE = 50000 def __init__(self, symbol, pos, height = 400, lower_radius = 50, upper_radius = 100): MovableObject.__init__(self) self.symbol = symbol self.area = 0 self.centroid = Vector(0, 0) self.bounds_min = Vector(0, 0) self.bounds_max = Vector(0, 0) self.height = height self.lower_radius = lower_radius self.upper_radius = upper_radius self.shifted_pos = Vector(0, 0) self.x0 = 0 self.y0 = 0 self.a1 = 0 self.b1 = 0 self.h1 = 0 self.a2 = 0 self.b2 = 0 self.h2 = 0 # For debugging, set this equal to True to draw a simple # trapezoid with a dot at the center. self.ellipses_and_letter_visible = True # Modify this so the centroid is really at (0, 0)? Maybe this isn't necessary. self.points = [ Vector(-self.upper_radius, -self.height/2.), Vector(self.upper_radius-self.lower_radius, self.height/2.), \ Vector(self.lower_radius, self.height/2.), Vector(-self.lower_radius, self.height/2.) ] #self.points = [ Vector(-self.upper_radius, -self.height * ( 4. * self.upper_radius + 2. * self.lower_radius)/(3. * (2. * self.upper_radius + 2. * self.lower_radius)) ), \ # # Vector(self.upper_radius, -self.height * ( 4. * self.upper_radius + 2. * self.lower_radius)/(3. * (2. * self.upper_radius + 2. * self.lower_radius)) ), \ # # Vector(self.lower_radius, self.height * ( 2. * self.upper_radius + 4. * self.lower_radius)/(3. * (2. * self.upper_radius + 2. * self.lower_radius)) ), \ # # Vector(-self.lower_radius, self.height * ( 2. * self.upper_radius + 4. * self.lower_radius)/(3. * (2. * self.upper_radius + 2. * self.lower_radius)) ) ] self.water_height = 0 self.water_lower_radius = lower_radius self.pos = pos self.volume = self.calculate_volume() self.water_volume = 0 self.animated_water_volume = 0 self.animated_water_height = 0 self.initial_volume_to_pour_out = 0 self.filling_from_faucet = False self.selectable = True self.symbol_visible = True self.rotatable = False self.contains_water = False self.full = False # Get the current bounding rectangle. self.calculate_bounds() def calculate_volume(self): return (math.pi * self.height / 3.0) * \ (self.lower_radius * self.lower_radius + self.lower_radius * self.upper_radius + self.upper_radius * self.upper_radius) def calculate_water_height(self, volume): a = (self.upper_radius - self.lower_radius)**2 b = 3.0 * self.lower_radius * (self.upper_radius - self.lower_radius) c = 3.0 * self.lower_radius ** 2 d = -3.0 * volume/(math.pi * self.height) solution = self.cubic(a, b, c, d) #if volume <= 0.0: if volume <= 0.0001: return 0.0 else: return solution[0] * self.height def fill_to_given_volume(self, volume): self.water_height = self.calculate_water_height(volume) self.water_volume = volume self.start_animating() # Solve the cubic equation. def cubic(self, a, b, c, d=None): from math import cos if a == 0: return self.quadratic(b, c, d)[0], self.quadratic(b, c, d)[1], 0 if d: # (ax^3 + bx^2 + cx + d = 0) a, b, c = b / float(a), c / float(a), d / float(a) t = a / 3.0 p, q = b - 3 * t**2, c - b * t + 2 * t**3 u, v = self.quadratic(q, -(p/3.0)**3) if type(u) == type(0j): # Complex cube root. r, w = polar(u.real, u.imag) y1 = 2 * self.cbrt(r) * cos(w / 3.0) else: # Real root. y1 = self.cbrt(u) + self.cbrt(v) y2, y3 = self.quadratic(y1, p + y1**2) return y1 - t, y2 - t, y3 - t # Solve the quadratic equation. def quadratic(self, a, b, c=None): import math, cmath if a == 0: return -c/float(b), 0 if c: # (ax^2 + bx + c = 0) a, b = b / float(a), c / float(a) t = a / 2.0 r = t**2 - b if r >= 0: # Real roots. y1 = math.sqrt(r) else: # Complex roots. y1 = cmath.sqrt(r) y2 = -y1 return y1 - t, y2 - t # Calculate a cube root. def cbrt(self, x): from math import pow if x >= 0: return pow(x, 1.0/3.0) else: return -pow(abs(x), 1.0/3.0) def draw_ellipse(self, cr, x, y, width, height): cr.new_sub_path() cr.save() if self.ellipses_and_letter_visible: cr.translate (x + width / 2., y) cr.scale(width / 2., height / 2.) cr.arc(0., 0., 1., 0., 2 * math.pi) cr.restore() def animate(self): if self.animated_water_volume < self.water_volume: self.animated_water_volume = min(self.animated_water_volume + VolumeObject.FILL_RATE, self.water_volume) self.animated_water_height = self.calculate_water_height(self.animated_water_volume) self.calculate_bounds() self.queue_draw() elif self.animated_water_volume > self.water_volume: #print "animating: self.animated_water_volume = ", self.animated_water_volume #print "animating: self.initial_volume_to_pour_out = ", self.initial_volume_to_pour_out # Have the angle change from 0 deg to 90 deg. angle = 90.0 * (1.0 - self.animated_water_volume/self.initial_volume_to_pour_out) self.rotate(angle * math.pi/180.0) theta = angle * math.pi/180.0 a1 = self.a1 b1 = self.b1 h1 = self.h1 a2 = self.a2 b2 = self.b2 h2 = self.h2 x_shift = - b2/2. - b1/2. y_shift = - h2/2. + h1/2. x_shift = - b2/2. - b1 * math.cos(theta) /2. - h1 * math.sin(theta) /2. y_shift = - h2/2. + h1 * math.cos(theta) /2. - b1 * math.sin(theta) /2. #print "x_shift =", x_shift #print "y_shift =", y_shift shifted_pos = Vector(self.x0 + x_shift, self.y0 + y_shift) self.move(shifted_pos) self.animated_water_volume = max(self.animated_water_volume - VolumeObject.FILL_RATE, self.water_volume) self.animated_water_height = self.calculate_water_height(self.animated_water_volume) self.calculate_bounds() self.queue_draw() else: self.calculate_bounds() self.queue_draw() self.stop_animating() self.filling_from_faucet = False def draw(self, cr): cr.translate(self.pos.x, self.pos.y) cr.rotate(self.angle) cr.scale(self.scale, self.scale) ul = Vector(-self.upper_radius, -self.height/2) ur = Vector(self.upper_radius, -self.height/2) lr = Vector(self.lower_radius, self.height/2) ll = Vector(-self.lower_radius, self.height/2) if self.animated_water_height > 1: t = self.animated_water_height / float(self.height) water_radius = self.upper_radius*t + self.lower_radius*(1-t) wl = Vector(-water_radius, self.height/2 - self.animated_water_height) wr = Vector(water_radius, self.height/2 - self.animated_water_height) cr.set_source_rgb(0.37, 0.74, 1.0) cr.line_to(wl.x, wl.y) cr.line_to(wr.x, wr.y) cr.line_to(lr.x, lr.y) cr.line_to(ll.x, ll.y) cr.close_path() self.draw_ellipse(cr, wl.x, wl.y, 2.0 * water_radius, water_radius/2.0) self.draw_ellipse(cr, ll.x, ll.y, 2.0 * self.lower_radius, self.lower_radius/2.0) cr.fill() cr.set_source_rgb(0.0, 0.0, 1.0) cr.set_line_width(4.0) self.draw_ellipse(cr, wl.x, wl.y, 2.0 * water_radius, water_radius/2.0) cr.stroke() # Draw the faucet filling. if self.filling_from_faucet: assert self.scale == 1.0 stream_x = self.container.problem.faucet_object.pos.x + FaucetObject.STREAM_X - self.pos.x stream_y = self.container.problem.faucet_object.pos.y + FaucetObject.STREAM_Y - self.pos.y cr.rectangle(stream_x, stream_y, FaucetObject.STREAM_WIDTH, self.height/2 - self.animated_water_height - stream_y) cr.set_source_rgb(0.37, 0.74, 1.0) cr.fill() # Now draw the shape of the container. if self.selected: cr.set_dash((10, 10), 0) cr.set_source_rgb(0.0, 0.0, 0.0) cr.set_line_width(4.0) cr.move_to(ul.x, ul.y) cr.line_to(ll.x, ll.y) cr.move_to(ur.x, ur.y) cr.line_to(lr.x, lr.y) if not self.ellipses_and_letter_visible: cr.move_to(ul.x, ur.y) cr.line_to(ur.x, ur.y) cr.move_to(ll.x, lr.y) cr.line_to(lr.x, lr.y) self.draw_ellipse(cr, ll.x, ll.y, 2.0 * self.lower_radius, self.lower_radius/2.0) self.draw_ellipse(cr, ul.x, ul.y, 2.0 * self.upper_radius, self.upper_radius/2.0) cr.stroke() # Draw the symbol (capital letter representing the shape's area). if self.symbol_visible and self.ellipses_and_letter_visible: cr.set_source_rgb(0, 0, 0) cr.set_font_size(50) x_bearing, y_bearing, width, height = cr.text_extents(self.symbol)[:4] cr.move_to(-x_bearing - width/2, -y_bearing - height/2) cr.show_text(self.symbol) # For debugging purposes, draw a dot to show the center of mass. if not self.ellipses_and_letter_visible: cr.save() cr.arc(0, 0, 4, 0, 2.*math.pi) cr.set_source_rgb(0., 0., 0.) cr.fill() cr.restore() def calculate_bounds(self): r = max(self.upper_radius, self.lower_radius) # 50 is a hack to account for a slight rotation halfsize = Vector(r + 2 + 50, self.height/2 + r/2 + 2) * self.scale self.bounds_min = self.pos - halfsize self.bounds_max = self.pos + halfsize # Include the stream when animating. if self.filling_from_faucet: stream_y = self.container.problem.faucet_object.pos.y + FaucetObject.STREAM_Y self.bounds_min.y = stream_y def get_bounds(self): return self.bounds_min, self.bounds_max