adg-web: allow custom canvas size
[adg-lua.git] / piston.lua
blobe7df06a4681a495ba0ad0149f9c668c673048409
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()
303 size.x = size.x + canvas:get_left_margin() + canvas:get_right_margin()
304 size.y = size.y + canvas:get_top_margin() + canvas:get_bottom_margin()
306 -- Create the cairo surface
307 local surface
308 if format == 'png' and cairo.ImageSurface then
309 surface = cairo.ImageSurface.create(cairo.Format.RGB24, size.x, size.y)
310 elseif format == 'svg' and cairo.SvgSurface then
311 surface = cairo.SvgSurface.create(file, size.x, size.y)
312 elseif format == 'pdf' and cairo.PdfSurface then
313 surface = cairo.PdfSurface.create(file, size.x, size.y)
314 elseif format == 'ps' and cairo.PsSurface then
315 -- Pull request: http://github.com/pavouk/lgi/pull/46
316 surface = cairo.PsSurface.create(file, size.x, size.y)
317 surface:dsc_comment('%%Title: adg-lua demonstration program')
318 surface:dsc_comment('%%Copyright: Copyleft (C) 2013 Fontana Nicola')
319 surface:dsc_comment('%%Orientation: Portrait')
320 surface:dsc_begin_setup()
321 surface:dsc_begin_page_setup()
322 surface:dsc_comment('%%IncludeFeature: *PageSize A4')
323 elseif not format then
324 format = '<nil>'
326 if not surface then
327 return nil, 'Requested format not supported (' .. format .. ')'
330 -- Render the canvas content
331 local cr = cairo.Context.create(surface)
332 canvas:render(cr)
333 local status
335 if cairo.Surface.get_type(surface) == 'IMAGE' then
336 status = cairo.Surface.write_to_png(surface, file)
337 else
338 cr:show_page()
339 status = cr.status
342 if status ~= 'SUCCESS' then
343 return nil, cairo.Status.to_string(cairo.Status[status])
345 end)
347 local function add_title_block(canvas)
348 canvas:set_title_block(Adg.TitleBlock {
349 title = '',
350 author = '',
351 date = '',
352 drawing = '',
353 logo = Adg.Logo {},
354 projection = Adg.Projection { scheme = Adg.ProjectionScheme.FIRST_ANGLE },
355 size = 'A4',
359 local function add_dimensions(canvas, model)
360 local body = model.body
361 local hole = model.hole
362 local dim
365 -- North
367 dim = Adg.LDim.new_full_from_model(body, '-D3I_X', '-D3F_X', '-D3F_Y', -math.pi/2)
368 dim:set_outside(Adg.ThreeState.OFF)
369 canvas:add(dim)
371 dim = Adg.LDim.new_full_from_model(body, '-D6I_X', '-D67', '-East', -math.pi/2)
372 dim:set_level(0)
373 dim:switch_extension1(false)
374 canvas:add(dim)
376 dim = Adg.LDim.new_full_from_model(body, '-D6I_X', '-D7F', '-East', -math.pi/2)
377 dim:set_limits('-0.06', nil)
378 canvas:add(dim)
380 dim = Adg.ADim.new_full_from_model(body, '-D6I_Y', '-D6F', '-D6F', '-D67', '-D6F')
381 dim:set_level(2)
382 canvas:add(dim)
384 dim = Adg.RDim.new_full_from_model(body, '-RD34', '-RD34_R', '-RD34_XY')
385 canvas:add(dim)
387 dim = Adg.LDim.new_full_from_model(body, '-DGROOVEI_X', '-DGROOVEF_X', '-DGROOVEX_POS', -math.pi/2)
388 canvas:add(dim)
390 dim = Adg.LDim.new_full_from_model(body, 'D2I', '-D2I', '-D2_POS', math.pi)
391 dim:set_limits('-0.1', nil)
392 dim:set_outside(Adg.ThreeState.OFF)
393 dim:set_value('\226\140\128 <>')
394 canvas:add(dim)
396 dim = Adg.LDim.new_full_from_model(body, 'DGROOVEI_Y', '-DGROOVEI_Y', '-DGROOVEY_POS', math.pi)
397 dim:set_limits('-0.1', nil)
398 dim:set_outside(Adg.ThreeState.OFF)
399 dim:set_value('\226\140\128 <>')
400 canvas:add(dim)
403 -- South
405 dim = Adg.ADim.new_full_from_model(body, 'D1F', 'D1I', 'D2I', 'D1F', 'D1F')
406 dim:set_level(2)
407 dim:switch_extension2(false)
408 canvas:add(dim)
410 dim = Adg.LDim.new_full_from_model(body, 'D1I', nil, 'West', math.pi / 2)
411 dim:set_ref2_from_model(hole, '-LHOLE')
412 dim:switch_extension1(false)
413 canvas:add(dim)
415 dim = Adg.LDim.new_full_from_model(body, 'D1I', 'DGROOVEI_X', 'West', math.pi / 2)
416 dim:switch_extension1(false)
417 dim:set_level(2)
418 canvas:add(dim)
420 dim = Adg.LDim.new_full_from_model(body, 'D4F', 'D6I_X', 'D4_POS', math.pi / 2)
421 dim:set_limits(nil, '+0.2')
422 dim:set_outside(Adg.ThreeState.OFF)
423 canvas:add(dim)
425 dim = Adg.LDim.new_full_from_model(body, 'D1F', 'D3I_X', 'D2_POS', math.pi / 2)
426 dim:set_level(2)
427 dim:switch_extension2(false)
428 dim:set_outside(Adg.ThreeState.OFF)
429 canvas:add(dim)
431 dim = Adg.LDim.new_full_from_model(body, 'D3I_X', 'D7F', 'East', math.pi / 2)
432 dim:set_limits(nil, '+0.1')
433 dim:set_level(2)
434 dim:set_outside(Adg.ThreeState.OFF)
435 dim:switch_extension2(false)
436 canvas:add(dim)
438 dim = Adg.LDim.new_full_from_model(body, 'D1I', 'D7F', 'D3F_Y', math.pi / 2)
439 dim:set_limits('-0.05', '+0.05')
440 dim:set_level(3)
441 canvas:add(dim)
443 dim = Adg.ADim.new_full_from_model(body, 'D4F', 'D4I', 'D5I', 'D4F', 'D4F')
444 dim:set_level(1.5)
445 dim:switch_extension2(false)
446 canvas:add(dim)
449 -- East
451 dim = Adg.LDim.new_full_from_model(body, 'D6F', '-D6F', 'East', 0)
452 dim:set_limits('-0.1', nil)
453 dim:set_level(4)
454 dim:set_value('\226\140\128 <>')
455 canvas:add(dim)
457 dim = Adg.LDim.new_full_from_model(body, 'D4F', '-D4F', 'East', 0)
458 dim:set_level(3)
459 dim:set_value('\226\140\128 <>')
460 canvas:add(dim)
462 dim = Adg.LDim.new_full_from_model(body, 'D5F', '-D5F', 'East', 0)
463 dim:set_limits('-0.1', nil)
464 dim:set_level(2)
465 dim:set_value('\226\140\128 <>')
466 canvas:add(dim)
468 dim = Adg.LDim.new_full_from_model(body, 'D7F', '-D7F', 'East', 0)
469 dim:set_value('\226\140\128 <>')
470 canvas:add(dim)
473 -- West
475 dim = Adg.LDim.new_full_from_model(hole, 'DHOLE', '-DHOLE', nil, math.pi)
476 dim:set_pos_from_model(body, '-West')
477 dim:set_value('\226\140\128 <>')
478 canvas:add(dim)
480 dim = Adg.LDim.new_full_from_model(body, 'D1I', '-D1I', '-West', math.pi)
481 dim:set_limits('-0.05', '+0.05')
482 dim:set_level(2)
483 dim:set_value('\226\140\128 <>')
484 canvas:add(dim)
486 dim = Adg.LDim.new_full_from_model(body, 'D3I_Y', '-D3I_Y', '-West', math.pi)
487 dim:set_limits('-0.25', nil)
488 dim:set_level(3)
489 dim:set_value('\226\140\128 <>')
490 canvas:add(dim)
493 function generator.view.detailed(part)
494 local canvas = Adg.Canvas {}
495 local model = part.model
497 add_title_block(canvas)
498 canvas:add(Adg.Stroke { trail = model.body })
499 canvas:add(Adg.Stroke { trail = model.edges })
500 canvas:add(Adg.Hatch { trail = model.hole })
501 canvas:add(Adg.Stroke { trail = model.hole })
502 canvas:add(Adg.Stroke {
503 trail = model.axis,
504 line_dress = Adg.Dress.LINE_AXIS
506 add_dimensions(canvas, model)
508 return canvas
512 -- CONTROLLER
513 -----------------------------------------------------------------
515 local controller = {}
517 function controller.new(data)
518 local part = {}
520 local function generate(class, method)
521 local constructor = generator[class][method]
522 local result = constructor and constructor(part) or false
523 part[class][method] = result
524 return result
527 -- data: numbers and strings needed to define the whole part
528 part.data = data or {}
530 -- model: different models (AdgModel instances) generated from data
531 part.model = {}
532 setmetatable(part.model, {
533 __index = function (self, key)
534 return generate('model', key)
538 -- view: drawings (AdgCanvas) availables for a single set of data
539 part.view = {}
540 setmetatable(part.view, {
541 __index = function (self, key)
542 return generate('view', key)
546 part.refresh = function (self)
547 -- Regenerate all the models
548 for _, model in pairs(self.model) do
549 model:reset()
550 model:regenerate(self)
551 model:changed()
554 -- Update the title block of all the views
555 for _, view in pairs(self.view) do
556 local title_block = view.title_block
557 for field in pairs(Adg.TitleBlock._property) do
558 local value = self.data[field:upper()]
559 if value then title_block[field] = value end
565 return part
569 return controller