doc: added adg-lua.tex for adg-demo.lua overview
[adg-lua.git] / piston.lua
blobe191783a24ce6ac7e142fa5da0165e8bfcce0b51
1 --[[
3 This file is part of adg-lua.
4 Copyright (C) 2012-2013 Nicola Fontana <ntd at entidi.it>
6 adg-lua is free software; you can redistribute it and/or modify
7 it under the terms of the GNU Lesser General Public License as
8 published by the Free Software Foundation; either version 2 of
9 the License, or (at your option) any later version.
11 adg-lua is distributed in the hope that it will be useful, but
12 WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU Lesser General Public License for more details.
16 You should have received a copy of the GNU Lesser General
17 Public License along with adg-lua; if not, write to the Free
18 Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19 Boston, MA 02110-1301, USA.
23 local lgi = require 'lgi'
24 local cairo = lgi.require 'cairo'
25 local Cpml = lgi.require 'Cpml'
26 local Adg = lgi.require 'Adg'
28 local SQRT3 = math.sqrt(3)
29 local generator = {}
32 -- Backward compatibility
34 if not cairo.Status.to_string then
35 -- Pull request: http://github.com/pavouk/lgi/pull/44
36 local core = require 'lgi.core'
37 local ffi = require 'lgi.ffi'
38 local ti = ffi.types
40 cairo._enum.Status.to_string = core.callable.new {
41 addr = cairo._module.cairo_status_to_string,
42 ret = ti.utf8,
43 cairo.Status
45 end
48 -- MODEL
49 -----------------------------------------------------------------
51 generator.model = {}
52 local constructor = {}
54 -- Inject the regenerate method into Adg.Model
56 -- Rebuilding the model *without* destroying it is the quickest method
57 -- to change a drawing: the notification mechanism will change only the
58 -- entities that effectively need to be modified.
60 -- Another (easier) option would be to regenerate everything - that is
61 -- models and views - from scratch.
62 rawset(Adg.Model, 'regenerate', function (model, part)
63 -- Call the original constructor of model, registered during the first call
64 -- to the same constructor, to regenerate it with the data stored in part.
65 constructor[model](part, model)
66 end)
68 function generator.model.hole(part, path)
69 path = path or Adg.Path {}
70 constructor[path] = generator.model.hole
72 local data = part.data
74 local pair = Cpml.Pair { x = data.LHOLE, y = 0 }
75 path:move_to(pair)
76 path:set_named_pair('LHOLE', pair)
78 pair.y = data.DHOLE / 2
79 pair.x = pair.x - pair.y / SQRT3
80 path:line_to(pair)
81 local edge = pair:dup()
83 pair.x = 0
84 path:line_to(pair)
85 path:set_named_pair('DHOLE', pair)
87 path:line_to_explicit(0, (data.D1 + data.DHOLE) / 4)
88 path:curve_to_explicit(data.LHOLE / 2, data.DHOLE / 2,
89 data.LHOLE + 2, data.D1 / 2,
90 data.LHOLE + 2, 0)
91 path:reflect()
92 path:close()
94 -- No need to incomodate an AdgEdge model for two reasons:
95 -- it is only a single line and it is always needed
96 path:move_to(edge)
97 edge.y = -edge.y
98 path:line_to(edge)
100 return path
103 local function add_groove(path, part)
104 local data = part.data
105 local pair = Cpml.Pair { x = data.ZGROOVE, y = data.D1 / 2 }
107 path:line_to(pair)
108 path:set_named_pair('DGROOVEI_X', pair)
110 pair.y = data.D3 / 2
111 path:set_named_pair('DGROOVEY_POS', pair)
113 pair.y = data.DGROOVE / 2
114 path:line_to(pair)
115 path:set_named_pair('DGROOVEI_Y', pair)
117 pair.x = pair.x + data.LGROOVE
118 path:line_to(pair)
120 pair.y = data.D3 / 2
121 path:set_named_pair('DGROOVEX_POS', pair)
123 pair.y = data.D1 / 2
124 path:line_to(pair)
125 path:set_named_pair('DGROOVEF_X', pair)
128 function generator.model.body(part, path)
129 path = path or Adg.Path {}
130 constructor[path] = generator.model.body
132 local data = part.data
134 local pair = Cpml.Pair { x = 0, y = data.D1 / 2 }
135 path:move_to(pair)
136 path:set_named_pair('D1I', pair)
138 if data.GROOVE then add_groove(path, part) end
140 pair.x = data.A - data.B - data.LD2
141 path:line_to(pair)
143 pair.y = data.D3 / 2
144 path:set_named_pair('D2_POS', pair)
146 pair.x = pair.x + (data.D1 - data.D2) / 2
147 pair.y = data.D2 / 2
148 path:line_to(pair)
149 path:set_named_pair('D2I', pair)
151 pair.x = data.A - data.B
152 path:line_to(pair)
153 path:fillet(0.4)
155 pair.x = data.A - data.B
156 pair.y = data.D3 / 2
157 path:line_to(pair)
158 path:set_named_pair('D3I', pair)
160 pair.x = data.A
161 path:set_named_pair('East', pair)
163 pair.x = 0
164 path:set_named_pair('West', pair)
166 path:chamfer(data.CHAMFER, data.CHAMFER)
168 pair.x = data.A - data.B + data.LD3
169 pair.y = data.D3 / 2
170 path:line_to(pair)
172 local primitive = path:over_primitive()
173 local tmp = primitive:put_point(0)
174 path:set_named_pair('D3I_X', tmp)
176 tmp = primitive:put_point(-1)
177 path:set_named_pair('D3I_Y', tmp)
179 path:chamfer(data.CHAMFER, data.CHAMFER)
181 pair.y = data.D4 / 2
182 path:line_to(pair)
184 primitive = path:over_primitive()
185 tmp = primitive:put_point(0)
186 path:set_named_pair('D3F_Y', tmp)
187 tmp = primitive:put_point(-1)
188 path:set_named_pair('D3F_X', tmp)
190 path:fillet(data.RD34)
192 pair.x = pair.x + data.RD34
193 path:set_named_pair('D4I', pair)
195 pair.x = data.A - data.C - data.LD5
196 path:line_to(pair)
197 path:set_named_pair('D4F', pair)
199 pair.y = data.D3 / 2
200 path:set_named_pair('D4_POS', pair)
202 primitive = path:over_primitive()
203 tmp = primitive:put_point(0)
204 tmp.x = tmp.x + data.RD34
205 path:set_named_pair('RD34', tmp)
207 tmp.x = tmp.x - math.cos(math.pi / 4) * data.RD34
208 tmp.y = tmp.y - math.sin(math.pi / 4) * data.RD34
209 path:set_named_pair('RD34_R', tmp)
211 tmp.x = tmp.x + data.RD34
212 tmp.y = tmp.y + data.RD34
213 path:set_named_pair('RD34_XY', tmp)
215 pair.x = pair.x + (data.D4 - data.D5) / 2
216 pair.y = data.D5 / 2
217 path:line_to(pair)
218 path:set_named_pair('D5I', pair)
220 pair.x = data.A - data.C
221 path:line_to(pair)
223 path:fillet(0.2)
225 pair.y = data.D6 / 2
226 path:line_to(pair)
228 primitive = path:over_primitive()
229 tmp = primitive:put_point(0)
230 path:set_named_pair('D5F', tmp)
232 path:fillet(0.1)
234 pair.x = pair.x + data.LD6
235 path:line_to(pair)
236 path:set_named_pair('D6F', pair)
238 primitive = path:over_primitive()
239 tmp = primitive:put_point(0)
240 path:set_named_pair('D6I_X', tmp)
242 primitive = path:over_primitive()
243 tmp = primitive:put_point(-1)
244 path:set_named_pair('D6I_Y', tmp)
246 pair.x = data.A - data.LD7
247 pair.y = pair.y - (data.C - data.LD7 - data.LD6) / SQRT3
248 path:line_to(pair)
249 path:set_named_pair('D67', pair)
251 pair.y = data.D7 / 2
252 path:line_to(pair)
254 pair.x = data.A
255 path:line_to(pair)
256 path:set_named_pair('D7F', pair)
258 path:reflect_explicit(1, 0)
259 path:close()
261 return path
264 function generator.model.edges(part, edges)
265 edges = edges or Adg.Edges {}
266 constructor[edges] = generator.model.edges
268 edges:set_source(part.model.body)
270 return edges
273 function generator.model.axis(part, path)
274 --[[
275 XXX: actually the end points can extend outside the body
276 only in local space. The proper extension values should be
277 expressed in global space but actually is impossible to
278 combine local and global space in the AdgPath API.
279 --]]
280 path = path or Adg.Path {}
281 constructor[path] = generator.model.axis
283 local data = part.data
285 path:move_to_explicit(-1, 0)
286 path:line_to_explicit(data.A + 1, 0)
288 return path
292 -- VIEW
293 -----------------------------------------------------------------
295 generator.view = {}
297 -- Inject the export method into Adg.Canvas
298 rawset(Adg.Canvas, 'export', function (canvas, file, format)
299 -- The not explicitely set, the export format is guessed from the file suffix
300 if not format then format = file:match('%.([^.]+)$') end
302 local size = canvas:get_size():dup()
303 size:transform(canvas:get_global_map())
305 -- Create the cairo surface
306 local surface
307 if format == 'png' and cairo.ImageSurface then
308 surface = cairo.ImageSurface.create(cairo.Format.RGB24, size.x, size.y)
309 elseif format == 'svg' and cairo.SvgSurface then
310 surface = cairo.SvgSurface.create(file, size.x, size.y)
311 elseif format == 'pdf' and cairo.PdfSurface then
312 surface = cairo.PdfSurface.create(file, size.x, size.y)
313 elseif format == 'ps' and cairo.PsSurface then
314 -- Pull request: http://github.com/pavouk/lgi/pull/46
315 surface = cairo.PsSurface.create(file, size.x, size.y)
316 surface:dsc_comment('%%Title: adg-lua demonstration program')
317 surface:dsc_comment('%%Copyright: Copyleft (C) 2013 Fontana Nicola')
318 surface:dsc_comment('%%Orientation: Portrait')
319 surface:dsc_begin_setup()
320 surface:dsc_begin_page_setup()
321 surface:dsc_comment('%%IncludeFeature: *PageSize A4')
322 elseif not format then
323 format = '<nil>'
325 if not surface then
326 return nil, 'Requested format not supported (' .. format .. ')'
329 -- Render the canvas content
330 local cr = cairo.Context.create(surface)
331 canvas:render(cr)
332 local status
334 if cairo.Surface.get_type(surface) == 'IMAGE' then
335 status = cairo.Surface.write_to_png(surface, file)
336 else
337 cr:show_page()
338 status = cr.status
341 if status ~= 'SUCCESS' then
342 return nil, cairo.Status.to_string(cairo.Status[status])
344 end)
346 local function add_title_block(canvas)
347 canvas:set_title_block(Adg.TitleBlock {
348 title = '',
349 author = '',
350 date = '',
351 drawing = '',
352 logo = Adg.Logo {},
353 projection = Adg.Projection { scheme = Adg.ProjectionScheme.FIRST_ANGLE },
354 size = 'A4',
358 local function add_dimensions(canvas, model)
359 local body = model.body
360 local hole = model.hole
361 local dim
364 -- North
366 dim = Adg.LDim.new_full_from_model(body, '-D3I_X', '-D3F_X', '-D3F_Y', -math.pi/2)
367 dim:set_outside(Adg.ThreeState.OFF)
368 canvas:add(dim)
370 dim = Adg.LDim.new_full_from_model(body, '-D6I_X', '-D67', '-East', -math.pi/2)
371 dim:set_level(0)
372 dim:switch_extension1(false)
373 canvas:add(dim)
375 dim = Adg.LDim.new_full_from_model(body, '-D6I_X', '-D7F', '-East', -math.pi/2)
376 dim:set_limits('-0.06', nil)
377 canvas:add(dim)
379 dim = Adg.ADim.new_full_from_model(body, '-D6I_Y', '-D6F', '-D6F', '-D67', '-D6F')
380 dim:set_level(2)
381 canvas:add(dim)
383 dim = Adg.RDim.new_full_from_model(body, '-RD34', '-RD34_R', '-RD34_XY')
384 canvas:add(dim)
386 dim = Adg.LDim.new_full_from_model(body, '-DGROOVEI_X', '-DGROOVEF_X', '-DGROOVEX_POS', -math.pi/2)
387 canvas:add(dim)
389 dim = Adg.LDim.new_full_from_model(body, 'D2I', '-D2I', '-D2_POS', math.pi)
390 dim:set_limits('-0.1', nil)
391 dim:set_outside(Adg.ThreeState.OFF)
392 dim:set_value('\226\140\128 <>')
393 canvas:add(dim)
395 dim = Adg.LDim.new_full_from_model(body, 'DGROOVEI_Y', '-DGROOVEI_Y', '-DGROOVEY_POS', math.pi)
396 dim:set_limits('-0.1', nil)
397 dim:set_outside(Adg.ThreeState.OFF)
398 dim:set_value('\226\140\128 <>')
399 canvas:add(dim)
402 -- South
404 dim = Adg.ADim.new_full_from_model(body, 'D1F', 'D1I', 'D2I', 'D1F', 'D1F')
405 dim:set_level(2)
406 dim:switch_extension2(false)
407 canvas:add(dim)
409 dim = Adg.LDim.new_full_from_model(body, 'D1I', nil, 'West', math.pi / 2)
410 dim:set_ref2_from_model(hole, '-LHOLE')
411 dim:switch_extension1(false)
412 canvas:add(dim)
414 dim = Adg.LDim.new_full_from_model(body, 'D1I', 'DGROOVEI_X', 'West', math.pi / 2)
415 dim:switch_extension1(false)
416 dim:set_level(2)
417 canvas:add(dim)
419 dim = Adg.LDim.new_full_from_model(body, 'D4F', 'D6I_X', 'D4_POS', math.pi / 2)
420 dim:set_limits(nil, '+0.2')
421 dim:set_outside(Adg.ThreeState.OFF)
422 canvas:add(dim)
424 dim = Adg.LDim.new_full_from_model(body, 'D1F', 'D3I_X', 'D2_POS', math.pi / 2)
425 dim:set_level(2)
426 dim:switch_extension2(false)
427 dim:set_outside(Adg.ThreeState.OFF)
428 canvas:add(dim)
430 dim = Adg.LDim.new_full_from_model(body, 'D3I_X', 'D7F', 'East', math.pi / 2)
431 dim:set_limits(nil, '+0.1')
432 dim:set_level(2)
433 dim:set_outside(Adg.ThreeState.OFF)
434 dim:switch_extension2(false)
435 canvas:add(dim)
437 dim = Adg.LDim.new_full_from_model(body, 'D1I', 'D7F', 'D3F_Y', math.pi / 2)
438 dim:set_limits('-0.05', '+0.05')
439 dim:set_level(3)
440 canvas:add(dim)
442 dim = Adg.ADim.new_full_from_model(body, 'D4F', 'D4I', 'D5I', 'D4F', 'D4F')
443 dim:set_level(1.5)
444 dim:switch_extension2(false)
445 canvas:add(dim)
448 -- East
450 dim = Adg.LDim.new_full_from_model(body, 'D6F', '-D6F', 'East', 0)
451 dim:set_limits('-0.1', nil)
452 dim:set_level(4)
453 dim:set_value('\226\140\128 <>')
454 canvas:add(dim)
456 dim = Adg.LDim.new_full_from_model(body, 'D4F', '-D4F', 'East', 0)
457 dim:set_level(3)
458 dim:set_value('\226\140\128 <>')
459 canvas:add(dim)
461 dim = Adg.LDim.new_full_from_model(body, 'D5F', '-D5F', 'East', 0)
462 dim:set_limits('-0.1', nil)
463 dim:set_level(2)
464 dim:set_value('\226\140\128 <>')
465 canvas:add(dim)
467 dim = Adg.LDim.new_full_from_model(body, 'D7F', '-D7F', 'East', 0)
468 dim:set_value('\226\140\128 <>')
469 canvas:add(dim)
472 -- West
474 dim = Adg.LDim.new_full_from_model(hole, 'DHOLE', '-DHOLE', nil, math.pi)
475 dim:set_pos_from_model(body, '-West')
476 dim:set_value('\226\140\128 <>')
477 canvas:add(dim)
479 dim = Adg.LDim.new_full_from_model(body, 'D1I', '-D1I', '-West', math.pi)
480 dim:set_limits('-0.05', '+0.05')
481 dim:set_level(2)
482 dim:set_value('\226\140\128 <>')
483 canvas:add(dim)
485 dim = Adg.LDim.new_full_from_model(body, 'D3I_Y', '-D3I_Y', '-West', math.pi)
486 dim:set_limits('-0.25', nil)
487 dim:set_level(3)
488 dim:set_value('\226\140\128 <>')
489 canvas:add(dim)
492 function generator.view.detailed(part)
493 local canvas = Adg.Canvas {}
494 local model = part.model
496 add_title_block(canvas)
497 canvas:add(Adg.Stroke { trail = model.body })
498 canvas:add(Adg.Stroke { trail = model.edges })
499 canvas:add(Adg.Hatch { trail = model.hole })
500 canvas:add(Adg.Stroke { trail = model.hole })
501 canvas:add(Adg.Stroke {
502 trail = model.axis,
503 line_dress = Adg.Dress.LINE_AXIS
505 add_dimensions(canvas, model)
507 return canvas
511 -- CONTROLLER
512 -----------------------------------------------------------------
514 local controller = {}
516 function controller.new(data)
517 local part = {}
519 local function generate(class, method)
520 local constructor = generator[class][method]
521 local result = constructor and constructor(part) or false
522 part[class][method] = result
523 return result
526 -- data: numbers and strings needed to define the whole part
527 part.data = data or {}
529 -- model: different models (AdgModel instances) generated from data
530 part.model = {}
531 setmetatable(part.model, {
532 __index = function (self, key)
533 return generate('model', key)
537 -- view: drawings (AdgCanvas) availables for a single set of data
538 part.view = {}
539 setmetatable(part.view, {
540 __index = function (self, key)
541 return generate('view', key)
545 part.refresh = function (self)
546 -- Regenerate all the models
547 for _, model in pairs(self.model) do
548 model:reset()
549 model:regenerate(self)
550 model:changed()
553 -- Update the title block of all the views
554 for _, view in pairs(self.view) do
555 local title_block = view.title_block
556 for field in pairs(Adg.TitleBlock._property) do
557 local value = self.data[field:upper()]
558 if value then title_block[field] = value end
564 return part
568 return controller