Bounds.py 20.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
# -*- coding: utf-8 -*-
"""
$Id$

Copyright 2011 Lars Kruse <devel@sumpfralle.de>

This file is part of PyCAM.

PyCAM 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.

PyCAM 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 PyCAM.  If not, see <http://www.gnu.org/licenses/>.
"""

import pycam.Plugins
# TODO: move Toolpath.Bounds here?
import pycam.Toolpath


28 29 30 31
_RELATIVE_UNIT = ("%", "mm")
_BOUNDARY_MODES = ("inside", "along", "around")


32 33 34 35
class Bounds(pycam.Plugins.ListPluginBase):

    UI_FILE = "bounds.ui"
    DEPENDS = ["Models"]
36
    CATEGORIES = ["Bounds"]
37
    COLUMN_REF = 0
38 39 40 41 42 43 44 45 46 47 48 49 50 51

    # mapping of boundary types and GUI control elements
    BOUNDARY_TYPES = {
            pycam.Toolpath.Bounds.TYPE_RELATIVE_MARGIN: "TypeRelativeMargin",
            pycam.Toolpath.Bounds.TYPE_CUSTOM: "TypeCustom"}
    CONTROL_BUTTONS = ("TypeRelativeMargin", "TypeCustom",
            "ToolLimit", "RelativeUnit", "BoundaryLowX",
            "BoundaryLowY", "BoundaryLowZ", "BoundaryHighX",
            "BoundaryHighY", "BoundaryHighZ")
    CONTROL_SIGNALS = ("toggled", "value-changed", "changed")
    CONTROL_GET = ("get_active", "get_value")
    CONTROL_SET = ("set_active", "set_value")

    def setup(self):
52
        self._event_handlers = []
53
        self.core.set("bounds", self)
54 55 56 57 58
        if self.gui:
            import gtk
            bounds_box = self.gui.get_object("BoundsBox")
            bounds_box.unparent()
            self.core.register_ui("main", "Bounds", bounds_box, 30)
59
            self._boundsview = self.gui.get_object("BoundsTable")
60 61 62
            self._gtk_handlers = []
            self._gtk_handlers.append((self._boundsview.get_selection(),
                    "changed", "bounds-selection-changed"))
63 64 65 66 67 68 69 70 71 72 73
            self._treemodel = self._boundsview.get_model()
            self._treemodel.clear()
            def update_model():
                if not hasattr(self, "_model_cache"):
                    self._model_cache = {}
                cache = self._model_cache
                for row in self._treemodel:
                    cache[row[self.COLUMN_REF]] = list(row)
                self._treemodel.clear()
                for index, item in enumerate(self):
                    if not id(item) in cache:
74
                        cache[id(item)] = [id(item)]
75
                    self._treemodel.append(cache[id(item)])
76
                self.core.emit_event("bounds-list-changed")
77 78 79 80 81 82
            self.register_model_update(update_model)
            for action, obj_name in ((self.ACTION_UP, "BoundsMoveUp"),
                    (self.ACTION_DOWN, "BoundsMoveDown"),
                    (self.ACTION_DELETE, "BoundsDelete")):
                self.register_list_action_button(action, self._boundsview,
                        self.gui.get_object(obj_name))
83 84
            self._gtk_handlers.append((self.gui.get_object("BoundsNew"),
                    "clicked", self._bounds_new))
85 86 87 88 89
            # model selector
            self.models_control = pycam.Gui.ControlsGTK.InputTable([],
                    change_handler=lambda *args: \
                        self.core.emit_event("bounds-changed"))
            self.gui.get_object("ModelsViewPort").add(self.models_control.get_widget())
90
            # quickly adjust the bounds via buttons
91 92 93 94 95 96 97 98 99 100 101
            for obj_name in ("MarginIncreaseX", "MarginIncreaseY",
                    "MarginIncreaseZ", "MarginDecreaseX", "MarginDecreaseY",
                    "MarginDecreaseZ", "MarginResetX", "MarginResetY",
                    "MarginResetZ"):
                axis = obj_name[-1].lower()
                if "Increase" in obj_name:
                    args = "+"
                elif "Decrease" in obj_name:
                    args = "-"
                else:
                    args = "0"
102 103
                self._gtk_handlers.append((self.gui.get_object(obj_name),
                        "clicked", self._adjust_bounds, axis, args))
104 105 106 107
            # connect change handler for boundary settings
            for axis in "XYZ":
                for value in ("Low", "High"):
                    obj_name = "Boundary%s%s" % (value, axis)
108 109
                    self._gtk_handlers.append((self.gui.get_object(obj_name),
                            "value-changed", "bounds-changed"))
110 111 112
            # register all controls
            for obj_name in self.CONTROL_BUTTONS:
                obj = self.gui.get_object(obj_name)
113
                if obj_name == "TypeRelativeMargin":
114 115
                    self._gtk_handlers.append((obj, "toggled",
                            self._switch_relative_custom))
116
                elif obj_name == "RelativeUnit":
117 118
                    self._gtk_handlers.append((obj, "changed",
                            self._switch_percent_absolute))
119
                else:
120 121
                    for signal in self.CONTROL_SIGNALS:
                        try:
122 123 124 125
                            handler = obj.connect(signal, lambda *args: None)
                            obj.disconnect(handler)
                            self._gtk_handlers.append((obj, signal,
                                    "bounds-changed"))
126 127 128 129
                            break
                        except TypeError:
                            continue
                    else:
130 131
                        self.log.info("Failed to connect to widget '%s'" % \
                                str(obj_name))
132
                        continue
133 134 135 136
            self._gtk_handlers.append((self.gui.get_object("NameCell"),
                    "edited", self._edit_bounds_name))
            self._event_handlers.extend((
                    ("bounds-selection-changed", self._switch_bounds),
137 138
                    ("bounds-changed", self._store_bounds_settings),
                    ("bounds-changed", self._trigger_table_update),
139 140
                    ("model-list-changed", self._update_model_list)))
            self.register_gtk_handlers(self._gtk_handlers)
141
            self._trigger_table_update()
142 143
            self._switch_bounds()
            self._update_model_list()
144 145
        self._event_handlers.append(("bounds-changed", "visual-item-updated"))
        self.register_event_handlers(self._event_handlers)
146
        self.register_state_item("bounds-list", self)
147 148
        self.core.register_namespace("bounds",
                pycam.Plugins.get_filter(self))
149 150 151
        return True

    def teardown(self):
152
        self.clear_state_items()
153
        self.core.unregister_namespace("bounds")
154 155
        if self.gui:
            self.core.unregister_ui("main", self.gui.get_object("BoundsBox"))
156 157
            self.unregister_gtk_handlers(self._gtk_handlers)
        self.unregister_event_handlers(self._event_handlers)
158
        self.core.set("bounds", None)
159 160
        while len(self) > 0:
            self.pop()
161 162 163 164 165 166 167

    def get_selected(self, index=False):
        return self._get_selected(self._boundsview, index=index)

    def select(self, bounds):
        if bounds in self:
            selection = self._boundsview.get_selection()
168
            index = [id(b) for b in self].index(id(bounds))
169 170 171 172
            selection.unselect_all()
            selection.select_path((index,))

    def get_selected_models(self, index=False):
173
        return self.models_control.get_value()
174 175

    def select_models(self, models):
176
        self.models_control.set_value(models)
177

178 179 180 181 182 183 184 185 186 187
    def _render_bounds_size(self, column, cell, model, m_iter):
        path = model.get_path(m_iter)
        bounds = self[path[0]]
        low, high = bounds.get_absolute_limits()
        if None in low or None in high:
            text = ""
        else:
            text = "%g x %g x %g" % tuple([high[i] - low[i] for i in range(3)])
        cell.set_property("text", text)

188 189 190 191 192
    def _render_bounds_name(self, column, cell, model, m_iter):
        path = model.get_path(m_iter)
        bounds = self[path[0]]
        cell.set_property("text", bounds["name"])

193 194 195
    def _trigger_table_update(self):
        self.gui.get_object("SizeColumn").set_cell_data_func(
                self.gui.get_object("SizeCell"), self._render_bounds_size)
196 197
        self.gui.get_object("NameColumn").set_cell_data_func(
                self.gui.get_object("NameCell"), self._render_bounds_name)
198

199
    def _update_model_list(self):
200 201 202
        models = self.core.get("models")
        choices = []
        for model in models:
203
            choices.append((model["name"], model))
204
        self.models_control.update_choices(choices)
205 206 207 208 209 210 211 212 213 214 215 216 217

    def _store_bounds_settings(self, widget=None):
        data = self.get_selected()
        control_box = self.gui.get_object("BoundsSettingsControlsBox")
        if data is None:
            control_box.hide()
            return
        else:
            for obj_name in self.CONTROL_BUTTONS:
                obj = self.gui.get_object(obj_name)
                for get_func in self.CONTROL_GET:
                    if hasattr(obj, get_func):
                        value = getattr(obj, get_func)()
218
                        data["parameters"][obj_name] = value
219 220 221
                        break
                else:
                    self.log.info("Failed to update value of control %s" % obj_name)
222
            data["parameters"]["Models"] = self.get_selected_models()
223 224 225 226 227 228 229 230
            control_box.show()
        self._hide_and_show_controls()

    def _hide_and_show_controls(self):
        # show the proper descriptive label for the current margin type
        relative_label = self.gui.get_object("MarginTypeRelativeLabel")
        custom_label = self.gui.get_object("MarginTypeCustomLabel")
        model_list = self.gui.get_object("ModelsTableFrame")
231 232 233 234
        percent_switch = self.gui.get_object("RelativeUnit")
        controls_x = self.gui.get_object("MarginControlsX")
        controls_y = self.gui.get_object("MarginControlsY")
        controls_z = self.gui.get_object("MarginControlsZ")
235 236 237 238
        if self.gui.get_object("TypeRelativeMargin").get_active():
            relative_label.show()
            custom_label.hide()
            model_list.show()
239
            percent_switch.show()
240 241 242
            controls_x.show()
            controls_y.show()
            controls_z.show()
243 244 245 246
        else:
            relative_label.hide()
            custom_label.show()
            model_list.hide()
247 248 249 250 251 252 253 254 255
            percent_switch.hide()
            controls_x.hide()
            controls_y.hide()
            controls_z.hide()

    def _switch_relative_custom(self, widget=None):
        bounds = self.get_selected()
        if not bounds:
            return
256
        models = [m.model for m in bounds["parameters"]["Models"]]
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
        if self.gui.get_object("TypeRelativeMargin").get_active():
            # no models are currently selected
            func_low = lambda value, axis: 0
            func_high = func_low
        else:
            # relative margins -> absolute coordinates
            # calculate the model bounds
            low, high = pycam.Geometry.Model.get_combined_bounds(models)
            if None in low or None in high:
                # zero-sized models -> no action
                return
            dim = []
            for axis in range(3):
                dim.append(high[axis] - low[axis])
            if self._is_percent():
                func_low = lambda value, axis: low[axis] - (value / 100.0 * dim[axis])
                func_high = lambda value, axis: high[axis] + (value / 100.0 * dim[axis])
            else:
                func_low = lambda value, axis: low[axis] - value
                func_high = lambda value, axis: high[axis] + value
            # absolute mode -> no models may be selected
            self._modelview.get_selection().unselect_all()
        for axis in "XYZ":
            for func, name in ((func_low, "BoundaryLow"),
                    (func_high, "BoundaryHigh")):
                try:
283
                    result = func(bounds["parameters"][name + axis], "XYZ".index(axis))
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
                except ZeroDivisionError:
                    # this happens for flat models
                    result = 0
                self.gui.get_object(name + axis).set_value(result)

    def _switch_percent_absolute(self, widget=None):
        """ re-calculate the values of the controls for the lower and upper
        margin of each axis. This is only necessary, if there are referenced
        models.
        Switching between percent and absolute values changes only numbers,
        but not the extend of margins.
        """
        bounds = self.get_selected()
        if not bounds:
            return
299
        models = [m.model for m in bounds["parameters"]["Models"]]
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315
        # calculate the model bounds
        low, high = pycam.Geometry.Model.get_combined_bounds(models)
        if None in low or None in high:
            # zero-sized models -> no action
            return
        dim = []
        for axis in range(3):
            dim.append(high[axis] - low[axis])
        if self._is_percent():
            # switched from absolute to percent
            func = lambda value, axis: value / dim[axis] * 100.0
        else:
            func = lambda value, axis: (value / 100.0) * dim[axis]
        for axis in "XYZ":
            for name in ("BoundaryLow", "BoundaryHigh"):
                try:
316
                    result = func(bounds["parameters"][name + axis], "XYZ".index(axis))
317 318 319 320 321 322 323
                except ZeroDivisionError:
                    # this happens for flat models
                    result = 0
                self.gui.get_object(name + axis).set_value(result)
        # Make sure that the new state of %/mm is always stored - even if no
        # control value has really changed (e.g. if all margins were zero).
        self._store_bounds_settings()
324 325 326 327 328 329 330

    def _adjust_bounds(self, widget, axis, change):
        bounds = self.get_selected()
        if not bounds:
            return
        axis_index = "xyz".index(axis)
        change_factor = {"0": 0, "+": 1, "-": -1}[change]
331
        if change == "0":
332 333
            bounds["parameters"]["BoundaryLow%s" % axis.upper()] = 0
            bounds["parameters"]["BoundaryHigh%s" % axis.upper()] = 0
334
        elif self._is_percent():
335
            # % margin
336 337
            bounds["parameters"]["BoundaryLow%s" % axis.upper()] += change_factor * 10
            bounds["parameters"]["BoundaryHigh%s" % axis.upper()] += change_factor * 10
338 339
        else:
            # absolute margin
340
            models = [m.model for m in self.get_selected_models()]
341 342 343 344
            model_low, model_high = pycam.Geometry.Model.get_combined_bounds(models)
            if None in model_low or None in model_high:
                return
            change_value = (model_high[axis_index] - model_low[axis_index]) * 0.1
345 346
            bounds["parameters"]["BoundaryLow%s" % axis.upper()] += change_value * change_factor
            bounds["parameters"]["BoundaryHigh%s" % axis.upper()] += change_value * change_factor
347 348 349
        self._update_controls()
        self.core.emit_event("bounds-changed")

350
    def _is_percent(self):
351
        return _RELATIVE_UNIT[self.gui.get_object("RelativeUnit").get_active()] == "%"
352 353 354 355 356 357 358

    def _update_controls(self):
        bounds = self.get_selected()
        control_box = self.gui.get_object("BoundsSettingsControlsBox")
        if not bounds:
            control_box.hide()
        else:
359
            self.unregister_gtk_handlers(self._gtk_handlers)
360
            for obj_name, value in bounds["parameters"].iteritems():
361 362 363 364 365 366 367 368 369 370 371 372 373
                if obj_name == "Models":
                    self.select_models(value)
                    continue
                obj = self.gui.get_object(obj_name)
                for set_func in self.CONTROL_SET:
                    if hasattr(obj, set_func):
                        if (value is False) and hasattr(obj, "get_group"):
                            # no "False" for radio buttons
                            pass
                        else:
                            getattr(obj, set_func)(value)
                        break
                else:
374 375
                    self.log.info("Failed to set value of control: %s" % \
                            obj_name)
376
            self.register_gtk_handlers(self._gtk_handlers)
377 378 379 380 381 382 383 384 385
            self._hide_and_show_controls()
            control_box.show()

    def _switch_bounds(self, widget=None):
        self._update_controls()
        # update the sensitivity of the lower z margin for contour models
        self.core.emit_event("bounds-changed")

    def _bounds_new(self, *args):
386 387 388 389 390
        bounds_names = [bounds["name"] for bounds in self]
        bounds_id = 1
        while ("Bounds #%d" % bounds_id) in bounds_names:
            bounds_id += 1
        new_bounds = BoundsDict(self.core, "Bounds #%d" % bounds_id)
391 392 393 394 395
        self.append(new_bounds)
        self.select(new_bounds)

    def _edit_bounds_name(self, cell, path, new_text):
        path = int(path)
396 397 398 399
        bounds_ref = self._treemodel[path][self.COLUMN_REF]
        bounds = [bound for bound in self if id(bound) == bounds_ref]
        if (new_text != bounds["name"]) and new_text:
            bounds["name"] = new_text
400 401


402
class BoundsDict(pycam.Plugins.ObjectWithAttributes):
403

404
    def __init__(self, core, name, *args, **kwargs):
405
        super(BoundsDict, self).__init__("bounds", *args, **kwargs)
406 407
        self["name"] = name
        self["parameters"] = {}
408
        self.core = core
409
        self["parameters"].update({
410 411 412 413 414 415 416 417
                "BoundaryLowX": 0,
                "BoundaryLowY": 0,
                "BoundaryLowZ": 0,
                "BoundaryHighX": 0,
                "BoundaryHighY": 0,
                "BoundaryHighZ": 0,
                "TypeRelativeMargin": True,
                "TypeCustom": False,
418 419 420 421
                # Use "list" conversion here: python 2.5 does not support
                # "index" for tuples.
                "RelativeUnit": list(_RELATIVE_UNIT).index("%"),
                "ToolLimit": list(_BOUNDARY_MODES).index("along"),
422
                "Models": [],
423
        })
424

425
    def get_absolute_limits(self, tool=None, models=None):
426
        default = (None, None, None), (None, None, None)
427 428 429 430 431
        get_low_value = lambda axis: \
                self["parameters"]["BoundaryLow%s" % "XYZ"[axis]]
        get_high_value = lambda axis: \
                self["parameters"]["BoundaryHigh%s" % "XYZ"[axis]]
        if self["parameters"]["TypeRelativeMargin"]:
432
            # choose the appropriate set of models
433
            if self["parameters"]["Models"]:
434
                # configured models always take precedence
435
                models = self["parameters"]["Models"]
436 437 438 439 440
            elif models:
                # use the supplied models (e.g. for toolpath calculation)
                pass
            else:
                # use all visible models -> for live visualization
441
                models = self.core.get("models").get_visible()
442
            low_model, high_model = pycam.Geometry.Model.get_combined_bounds(
443
                    [model.model for model in models])
444 445 446
            if None in low_model or None in high_model:
                # zero-sized models -> no action
                return default
447
            is_percent = _RELATIVE_UNIT[self["parameters"]["RelativeUnit"]] == "%"
448 449 450 451 452 453 454 455 456 457 458 459 460 461 462
            low, high = [], []
            if is_percent:
                for axis in range(3):
                    dim = high_model[axis] - low_model[axis]
                    low.append(low_model[axis] - (get_low_value(axis) / 100.0 * dim))
                    high.append(high_model[axis] + (get_high_value(axis) / 100.0 * dim))
            else:
                for axis in range(3):
                    low.append(low_model[axis] - get_low_value(axis))
                    high.append(high_model[axis] + get_high_value(axis))
        else:
            low, high = [], []
            for axis in range(3):
                low.append(get_low_value(axis))
                high.append(get_high_value(axis))
463
        tool_limit = _BOUNDARY_MODES[self["parameters"]["ToolLimit"]]
464 465
        # apply inside/along/outside if a tool is given
        if tool and (tool_limit != "along"):
466 467 468 469 470 471 472 473 474
            tool_radius = tool["parameters"]["radius"]
            if tool_limit == "inside":
                offset = -tool_radius
            else:
                offset = tool_radius
            # apply offset only for x and y
            for index in range(2):
                low[index] -= offset
                high[index] += offset
475
        return low, high
476