disp3D 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/README.rdoc +73 -1
  2. data/VERSION +1 -1
  3. data/disp3D.gemspec +97 -0
  4. data/example/stl_viewer/app_model.rb +8 -0
  5. data/example/stl_viewer/document.rb +15 -0
  6. data/example/stl_viewer/document_ctrl.rb +62 -0
  7. data/example/stl_viewer/gl_ctrl.rb +15 -0
  8. data/example/stl_viewer/main.rb +120 -0
  9. data/example/stl_viewer/mesh_info.rb +12 -0
  10. data/example/stl_viewer/stl_viewer.rb +9 -0
  11. data/lib/camera.rb +49 -35
  12. data/lib/disp3D.rb +19 -3
  13. data/lib/dsl.rb +18 -0
  14. data/lib/gl_view.rb +80 -0
  15. data/lib/glut_window.rb +60 -0
  16. data/lib/light.rb +49 -0
  17. data/lib/manipulator.rb +92 -21
  18. data/lib/node.rb +15 -9
  19. data/lib/node_arrows.rb +68 -0
  20. data/lib/node_collection.rb +14 -3
  21. data/lib/node_leaf.rb +80 -0
  22. data/lib/node_lines.rb +4 -7
  23. data/lib/node_points.rb +9 -11
  24. data/lib/node_polylines.rb +34 -0
  25. data/lib/node_tea_pod.rb +9 -2
  26. data/lib/node_text.rb +17 -0
  27. data/lib/node_tris.rb +24 -0
  28. data/lib/picked_result.rb +15 -0
  29. data/lib/picker.rb +56 -0
  30. data/lib/qt_widget_gl.rb +80 -0
  31. data/lib/scene_graph.rb +5 -0
  32. data/lib/stl.rb +92 -0
  33. data/lib/util.rb +18 -0
  34. data/test/test_data/binary_test.stl +0 -0
  35. data/test/test_data/cube-ascii.stl +86 -0
  36. data/test/test_dsl.rb +8 -0
  37. data/test/test_glut_window.rb +113 -0
  38. data/test/test_qtgl.rb +26 -0
  39. data/test/test_stl.rb +12 -0
  40. data/test/test_tea_pod.rb +13 -2
  41. metadata +41 -19
  42. data/lib/helloworld.rb +0 -112
  43. data/lib/view.rb +0 -47
  44. data/test/test_line.rb +0 -8
  45. data/test/test_lines.rb +0 -17
  46. data/test/test_point.rb +0 -25
  47. data/test/test_points.rb +0 -27
data/README.rdoc CHANGED
@@ -1,6 +1,78 @@
1
1
  = disp3D
2
+ 本ライブラリは、Qtと組み合わせて、3次元の表示が簡単に可能なフレームワークを提供する目的で開発されています。ターゲットは、簡単なアプリ、プロトタイピング、学術利用などを想定しています。
2
3
 
3
- This library provide 3D GUI framework.
4
+ 以下のような特徴を持つようなライブラリを目指します。
5
+ - シンプルで使いやすく!
6
+ - Rubyでの開発が可能
7
+ - windows,mac,linuxなどプラットフォームを問わない
8
+
9
+ また、ゲーム、CGや大規模モデルなどは当面の目標からは外れます。
10
+
11
+ == インストール
12
+ ライブラリは以下の3つのgemに依存しています。
13
+ - gmath3D
14
+ - ruby-opengl
15
+ - qtbindings()
16
+
17
+ === gmath3D
18
+ gemからインストールが可能です
19
+
20
+ gem install GMath3D
21
+
22
+ === ruby-opengl
23
+ openglを扱う上では、必須となります。
24
+
25
+ gemでのインストールが失敗する可能性があります。
26
+ https://github.com/toshi0328/ruby-opengl
27
+ の内容をビルド、インストールします。
28
+
29
+ === qtbindings
30
+ Qtを利用する場合に必要となります。
31
+
32
+ gemでのインストールが失敗する可能性があります。
33
+ https://github.com/toshi0328/qtbindings
34
+ の内容をビルド、インストールします。
35
+
36
+ == 使い方
37
+ === 例
38
+ 1.点や線を3次元ビュー上に追加し、表示する
39
+ 開発中
40
+
41
+ 2.Qtアプリケーションとして開発をする
42
+ 開発中
43
+
44
+ 3.STLファイルを読み込み、表示する
45
+ 開発中
46
+
47
+ 4.表示要素をマウス操作で取得する
48
+ 開発中
49
+
50
+ === データ構造
51
+ 中心となるクラスは、GLViewおよびSceneGraphです。
52
+ 執筆中
53
+
54
+ == 今後の課題
55
+ === 開発項目
56
+ - デモアプリ(STLビューワー)
57
+  - QTでのアプリケーション設計
58
+  - tri_nodeの法線方向指定
59
+ - インディケート処理の追加
60
+ - trackball manipulatorを洗練させる
61
+  - 回転中心を変えるとか・・・
62
+ - DSLの設計
63
+ - 自動テスト構築
64
+ - Windowsでの利用(なぜか、インストールでエラー・・・)
65
+ - ライティングの工夫、表示の洗練
66
+ - ドキュメント整理(html?)
67
+ - テクスチャの貼り付け
68
+ - データの可視化としての応用
69
+ - アニメーション
70
+ - モバイル応用
71
+
72
+ === 他ライブラリとの組み合わせ
73
+ - 3次元画像処理,pclの利用
74
+ - 医用応用として、DICOMパーサー(Ruby-DICOM)の利用
75
+ - 2次元画像処理、RMagicに任せる・・・(Ruby-DICOMとのつなぎなど)
4
76
 
5
77
  == Copyright
6
78
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.1.1
data/disp3D.gemspec ADDED
@@ -0,0 +1,97 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{disp3D}
8
+ s.version = "0.1.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Toshiyasu Shimizu"]
12
+ s.date = %q{2011-11-03}
13
+ s.description = %q{disp3D provide 3D GUI framework}
14
+ s.email = %q{toshi0328@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ "Gemfile",
22
+ "Gemfile.lock",
23
+ "LICENSE.txt",
24
+ "README.rdoc",
25
+ "Rakefile",
26
+ "VERSION",
27
+ "disp3D.gemspec",
28
+ "example/stl_viewer/app_model.rb",
29
+ "example/stl_viewer/document.rb",
30
+ "example/stl_viewer/document_ctrl.rb",
31
+ "example/stl_viewer/gl_ctrl.rb",
32
+ "example/stl_viewer/main.rb",
33
+ "example/stl_viewer/mesh_info.rb",
34
+ "example/stl_viewer/stl_viewer.rb",
35
+ "lib/camera.rb",
36
+ "lib/disp3D.rb",
37
+ "lib/dsl.rb",
38
+ "lib/gl_view.rb",
39
+ "lib/glut_window.rb",
40
+ "lib/light.rb",
41
+ "lib/manipulator.rb",
42
+ "lib/node.rb",
43
+ "lib/node_arrows.rb",
44
+ "lib/node_collection.rb",
45
+ "lib/node_leaf.rb",
46
+ "lib/node_lines.rb",
47
+ "lib/node_points.rb",
48
+ "lib/node_polylines.rb",
49
+ "lib/node_tea_pod.rb",
50
+ "lib/node_text.rb",
51
+ "lib/node_tris.rb",
52
+ "lib/picked_result.rb",
53
+ "lib/picker.rb",
54
+ "lib/qt_widget_gl.rb",
55
+ "lib/scene_graph.rb",
56
+ "lib/stl.rb",
57
+ "lib/util.rb",
58
+ "test/helper.rb",
59
+ "test/test_data/binary_test.stl",
60
+ "test/test_data/cube-ascii.stl",
61
+ "test/test_dsl.rb",
62
+ "test/test_glut_window.rb",
63
+ "test/test_qtgl.rb",
64
+ "test/test_stl.rb",
65
+ "test/test_tea_pod.rb"
66
+ ]
67
+ s.homepage = %q{http://github.com/toshi0328/disp3D}
68
+ s.licenses = ["MIT"]
69
+ s.require_paths = ["lib"]
70
+ s.rubygems_version = %q{1.6.2}
71
+ s.summary = %q{disp3D provide 3D GUI framework.}
72
+
73
+ if s.respond_to? :specification_version then
74
+ s.specification_version = 3
75
+
76
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
77
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
78
+ s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
79
+ s.add_development_dependency(%q<gmath3D>, [">= 0"])
80
+ s.add_development_dependency(%q<ruby-opengl>, ["= 0.60.1"])
81
+ s.add_development_dependency(%q<qtbindings>, ["= 4.6.3.4"])
82
+ else
83
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
84
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
85
+ s.add_dependency(%q<gmath3D>, [">= 0"])
86
+ s.add_dependency(%q<ruby-opengl>, ["= 0.60.1"])
87
+ s.add_dependency(%q<qtbindings>, ["= 4.6.3.4"])
88
+ end
89
+ else
90
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
91
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
92
+ s.add_dependency(%q<gmath3D>, [">= 0"])
93
+ s.add_dependency(%q<ruby-opengl>, ["= 0.60.1"])
94
+ s.add_dependency(%q<qtbindings>, ["= 4.6.3.4"])
95
+ end
96
+ end
97
+
@@ -0,0 +1,8 @@
1
+ require 'stl_viewer'
2
+
3
+ #TODO all model should have @dirty flg...
4
+ class AppModel
5
+ def initialize
6
+ @dirty = false
7
+ end
8
+ end
@@ -0,0 +1,15 @@
1
+ require 'stl_viewer'
2
+
3
+ class Document
4
+ def initialize
5
+ @tri_mesh_info_list = []
6
+ # @dirty = false
7
+ end
8
+
9
+ def add_tri_mesh!(tri_mesh)
10
+ # @dirty = true
11
+ mesh_info = MeshInfo.new(tri_mesh)
12
+ @tri_mesh_info_list.push(mesh_info)
13
+ return mesh_info
14
+ end
15
+ end
@@ -0,0 +1,62 @@
1
+ require 'stl_viewer'
2
+
3
+ class DocumentCtrl
4
+ attr_reader :document
5
+
6
+ def initialize(main_window)
7
+ @document = nil # this is THE ROOT DOCUMENT of this application
8
+ @main_window = main_window
9
+ @gl_ctrl = GLCtrl.new(main_window.gl_widget)
10
+ end
11
+
12
+ def new
13
+ if (@document.nil? || may_be_save)
14
+ @document = Document.new()
15
+ end
16
+ end
17
+
18
+ def open
19
+ #TODO implement!
20
+ end
21
+
22
+ def save
23
+ #TODO implement! return true if success
24
+ return true
25
+ end
26
+
27
+ def add_stl
28
+ if(@document.nil?)
29
+ new()
30
+ end
31
+
32
+ file_name = Qt::FileDialog.getOpenFileName(@main_window)
33
+ if !file_name.nil?
34
+ stl = STL.new()
35
+ if stl.parse(file_name)
36
+ added = @document.add_tri_mesh!(stl.tri_mesh)
37
+ if !added.nil?
38
+ @gl_ctrl.add2scenegraph(added)
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ private
45
+ def may_be_save
46
+ if @document.dirty
47
+ ret = Qt::MessageBox::warning(@main_window, tr("STL Viewer"),
48
+ tr("The document has been modified. \n" +
49
+ "Do you want to save your change?"),
50
+ Qt::MessageBox::Yes | Qt::MessageBox::Defaut,
51
+ Qt::MessageBox::No,
52
+ Qt::MessageBox::Cancel | Qt::MessageBox::Escape)
53
+ if ret == Qt::MessageBox::Yes
54
+ return save()
55
+ elsif ret == Qt::MessageBox::Cancel
56
+ return false
57
+ end
58
+ end
59
+ return true
60
+ end
61
+
62
+ end
@@ -0,0 +1,15 @@
1
+ require 'stl_viewer'
2
+
3
+ class GLCtrl
4
+ def initialize(gl_widget)
5
+ @gl_widget = gl_widget
6
+ end
7
+
8
+ def add2scenegraph(mesh_info)
9
+ mesh_info.mesh_node = Disp3D::NodeTris.new(mesh_info.mesh_geom)
10
+ mesh_info.mesh_node.material_color = [1,0,0,1]
11
+ @gl_widget.world_scene_graph.add(mesh_info.mesh_node)
12
+ @gl_widget.fit
13
+ @gl_widget.updateGL
14
+ end
15
+ end
@@ -0,0 +1,120 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '../..', 'lib'))
3
+
4
+ require 'Qt'
5
+ require 'qt_widget_gl'
6
+ require 'stl_viewer'
7
+
8
+ class STLViewerWindow < Qt::MainWindow
9
+ attr_reader :gl_widget
10
+
11
+ slots 'new()',
12
+ 'open()',
13
+ 'save()',
14
+ 'add_stl()',
15
+ 'about()',
16
+ 'aboutQt()'
17
+
18
+ def initialize(parent = nil)
19
+ super
20
+
21
+ w = Qt::Widget.new
22
+ setCentralWidget(w)
23
+
24
+ topFiller = Qt::Widget.new
25
+ topFiller.setSizePolicy(Qt::SizePolicy::Expanding, Qt::SizePolicy::Expanding)
26
+
27
+ @gl_widget = QtWidgetGL.new(self)
28
+ @gl_widget.width = 600
29
+ @gl_widget.height = 400
30
+
31
+ bottomFiller = Qt::Widget.new
32
+ bottomFiller.setSizePolicy(Qt::SizePolicy::Expanding, Qt::SizePolicy::Expanding)
33
+
34
+ vbox = Qt::VBoxLayout.new
35
+ vbox.margin = 5
36
+ vbox.addWidget(topFiller)
37
+ vbox.addWidget(@gl_widget)
38
+ vbox.addWidget(bottomFiller)
39
+ w.layout = vbox
40
+
41
+ createActions()
42
+ createMenus()
43
+
44
+ setWindowTitle(tr("STL Viewer"))
45
+ setMinimumSize(160, 160)
46
+
47
+ # controllers
48
+ @doc_ctrl = DocumentCtrl.new(self)
49
+ end
50
+
51
+ def createActions()
52
+ @newAct = Qt::Action.new(tr("&New..."), self)
53
+ @newAct.shortcut = Qt::KeySequence.new( tr("Ctrl+N") )
54
+ @newAct.statusTip = tr("Create new file")
55
+ connect(@newAct, SIGNAL('triggered()'), self, SLOT('new()'))
56
+
57
+ @openAct = Qt::Action.new(tr("&Open..."), self)
58
+ @openAct.shortcut = Qt::KeySequence.new( tr("Ctrl+O") )
59
+ @openAct.statusTip = tr("Open an existing file")
60
+ connect(@openAct, SIGNAL('triggered()'), self, SLOT('open()'))
61
+
62
+ @saveAct = Qt::Action.new(tr("&Save"), self)
63
+ @saveAct.shortcut = Qt::KeySequence.new( tr("Ctrl+S") )
64
+ @saveAct.statusTip = tr("Save the document to disk")
65
+ connect(@saveAct, SIGNAL('triggered()'), self, SLOT('save()'))
66
+
67
+ @addStlAct = Qt::Action.new(tr("&Add STL"), self)
68
+ @addStlAct.shortcut = Qt::KeySequence.new( tr("Ctrl+A") )
69
+ @addStlAct.statusTip = tr("Add STL file")
70
+ connect(@addStlAct, SIGNAL('triggered()'), self, SLOT('add_stl()'))
71
+
72
+ @aboutAct = Qt::Action.new(tr("&About"), self)
73
+ @aboutAct.statusTip = tr("Show the application's About box")
74
+ connect(@aboutAct, SIGNAL('triggered()'), self, SLOT('about()'))
75
+
76
+ @aboutQtAct = Qt::Action.new(tr("About &Qt"), self)
77
+ @aboutQtAct.statusTip = tr("Show the Qt library's About box")
78
+ connect(@aboutQtAct, SIGNAL('triggered()'), $qApp, SLOT('aboutQt()'))
79
+ end
80
+
81
+ def createMenus()
82
+ @fileMenu = menuBar().addMenu(tr("&File"))
83
+ @fileMenu.addAction(@newAct)
84
+ @fileMenu.addAction(@openAct)
85
+ @fileMenu.addAction(@saveAct)
86
+ @fileMenu.addAction(@addStlAct)
87
+
88
+ @helpMenu = menuBar().addMenu(tr("&Help"))
89
+ @helpMenu.addAction(@aboutAct)
90
+ @helpMenu.addAction(@aboutQtAct)
91
+ end
92
+
93
+ def new()
94
+ @doc_ctrl.new
95
+ end
96
+
97
+ def open()
98
+ @doc_ctrl.open
99
+ end
100
+
101
+ def save()
102
+ @doc_ctrl.save
103
+ end
104
+
105
+ def add_stl()
106
+ @doc_ctrl.add_stl
107
+ end
108
+
109
+ def about()
110
+ Qt::MessageBox.about(self, tr("About Menu"),
111
+ tr("STL Viewer, Demo application using <b>disp3D</b> library."))
112
+ end
113
+ end
114
+
115
+ # start application
116
+ app = Qt::Application.new(ARGV)
117
+ window = STLViewerWindow.new
118
+ window.show
119
+ app.exec
120
+
@@ -0,0 +1,12 @@
1
+ require 'stl_viewer'
2
+
3
+ class MeshInfo
4
+ attr_reader :mesh_geom
5
+ attr_accessor :mesh_node
6
+
7
+ def initialize(mesh)
8
+ @mesh_geom = mesh
9
+ @mesh_node = nil
10
+ end
11
+
12
+ end
@@ -0,0 +1,9 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+
3
+ require 'app_model'
4
+
5
+ require 'document'
6
+ require 'mesh_info'
7
+
8
+ require 'document_ctrl'
9
+ require 'gl_ctrl'
data/lib/camera.rb CHANGED
@@ -2,56 +2,70 @@ require 'disp3D'
2
2
 
3
3
  module Disp3D
4
4
  class Camera
5
- attr_accessor :rotX
6
- attr_accessor :rotY
5
+ attr_accessor :rotation
6
+ attr_accessor :translate
7
+ attr_accessor :center
8
+ attr_accessor :scale
9
+ attr_accessor :is_orth
7
10
 
8
- LIGHT_POSITION = [0.25, 1.0, 0.25, 0.0]
9
- LIGHT_DIFFUSE = [1.0, 1.0, 1.0]
10
- LIGHT_AMBIENT = [0.25, 0.25, 0.25]
11
- LIGHT_SPECULAR = [1.0, 1.0, 1.0]
12
-
13
- MAT_DIFFUSE = [0.0, 0.0, 0.0]
14
- MAT_AMBIENT = [0.25, 0.25, 0.25]
15
- MAT_SPECULAR = [1.0, 1.0, 1.0]
16
- MAT_SHININESS = [32.0]
11
+ def initialize()
12
+ @rotation = Quat.from_axis(Vector3.new(1,0,0),0)
13
+ @translate = nil
14
+ @eye = Vector3.new(0,0,5)
15
+ @center = Vector3.new(0,0,0)
16
+ @scale = 1
17
+ @angle = 30
18
+ @is_orgh = false
19
+ @near = 0.1
20
+ @far = 100.0
21
+ end
17
22
 
18
23
  def reshape(w,h)
19
- GL.Viewport(0,0,w,h)
20
-
24
+ GL.Viewport(0.0,0.0,w,h)
21
25
  GL.MatrixMode(GL::GL_PROJECTION)
22
26
  GL.LoadIdentity()
23
- GLU.Perspective(45.0, w.to_f()/h.to_f(), 0.1, 100.0)
24
- # GL.Ortho(-1,1,-1,1,2,4)
27
+ set_screen(w,h)
25
28
  end
26
29
 
27
30
  def display()
28
31
  GL.MatrixMode(GL::GL_MODELVIEW)
29
32
  GL.LoadIdentity()
30
- GLU.LookAt(0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0)
31
-
32
- GL.Lightfv(GL::GL_LIGHT0, GL::GL_POSITION, LIGHT_POSITION)
33
- GL.Lightfv(GL::GL_LIGHT0, GL::GL_DIFFUSE, LIGHT_DIFFUSE)
34
- GL.Lightfv(GL::GL_LIGHT0, GL::GL_AMBIENT, LIGHT_AMBIENT)
35
- GL.Lightfv(GL::GL_LIGHT0, GL::GL_SPECULAR, LIGHT_SPECULAR)
33
+ GLU.LookAt(@eye.x, @eye.y, @eye.z, @center.x, @center.y, @center.z, 0.0, 1.0, 0.0)
36
34
 
37
- GL.Materialfv(GL::GL_FRONT, GL::GL_DIFFUSE, MAT_DIFFUSE)
38
- GL.Materialfv(GL::GL_FRONT, GL::GL_AMBIENT, MAT_AMBIENT)
39
- GL.Materialfv(GL::GL_FRONT, GL::GL_SPECULAR, MAT_SPECULAR)
40
- GL.Materialfv(GL::GL_FRONT, GL::GL_SHININESS, MAT_SHININESS)
35
+ GL.Translate(translate.x, translate.y, translate.z) if(@translate)
36
+ rot_mat = Matrix.from_quat(@rotation)
37
+ rot_mat_array = [
38
+ [rot_mat[0,0], rot_mat[0,1], rot_mat[0,2], 0],
39
+ [rot_mat[1,0], rot_mat[1,1], rot_mat[1,2], 0],
40
+ [rot_mat[2,0], rot_mat[2,1], rot_mat[2,2], 0],
41
+ [0,0,0,1]]
41
42
 
42
- GL.Clear(GL::GL_COLOR_BUFFER_BIT | GL::GL_DEPTH_BUFFER_BIT)
43
-
44
- GL.Rotate(@rotX, 1, 0, 0)
45
- GL.Rotate(@rotY, 0, 1, 0)
43
+ GL.MultMatrix(rot_mat_array)
44
+ GL.Scale(@scale, @scale, @scale)
46
45
  end
47
46
 
48
- def initialize()
49
- @rotY = 0
50
- @rotX = 0
51
- GL.Enable(GL::GL_LIGHTING)
52
- GL.Enable(GL::GL_LIGHT0)
47
+ def fit(radius, width, height)
48
+ eye_z = radius / Math.sin(@angle/2.0*Math::PI/180.0)
49
+ @eye = Vector3.new(0,0,eye_z)
50
+ @near = radius - 1
51
+ @far = eye_z + radius
52
+ if @is_orgh
53
+ min_screen = [width, height].min
54
+ @scale = (min_screen.to_f/2.0)/radius
55
+ else
56
+ @scale = 1.0
57
+ end
58
+ set_screen(width,height)
59
+ end
53
60
 
54
- GLUT.ReshapeFunc(method(:reshape).to_proc())
61
+ def set_screen(w,h)
62
+ @aspect = w.to_f()/h.to_f()
63
+ if @is_orgh
64
+ GL.Ortho(-w/2.0, w/2.0, -h/2.0, h/2.0, -@far*@scale*10, @far*@scale*10)
65
+ else
66
+ GLU.Perspective(@angle, @aspect, @near, @far)
67
+ end
55
68
  end
69
+
56
70
  end
57
71
  end