From e753ec6b4fabb14b24c92308cfe2434b8bdf8e5e Mon Sep 17 00:00:00 2001 From: Philipp Engel Date: Fri, 7 Nov 2014 09:23:12 -0800 Subject: [PATCH] * added a svg to gcode plugin (copied from cura-plugin, so there is probably still a lot of unused code in it) * added 'svg' as a supported model file extension / format * added a new dialog for customizing gcode conversion of svgs --- src/octoprint/filemanager/__init__.py | 2 +- src/octoprint/plugins/svgtogcode/__init__.py | 363 ++++++++ src/octoprint/plugins/svgtogcode/profile.py | 825 ++++++++++++++++++ .../svgtogcode/profiles/default.profile.yaml | 4 + .../svgtogcode/static/css/svgtogcode.css | 1 + .../svgtogcode/static/js/svgtogcode.js | 191 ++++ .../svgtogcode/static/less/svgtogcode.less | 29 + .../svgtogcode_settings_dialog.jinja2 | 247 ++++++ .../static/js/app/viewmodels/files.js | 17 +- src/octoprint/templates/dialogs.jinja2 | 37 + src/octoprint/templates/index.jinja2 | 10 + 11 files changed, 1724 insertions(+), 2 deletions(-) create mode 100644 src/octoprint/plugins/svgtogcode/__init__.py create mode 100644 src/octoprint/plugins/svgtogcode/profile.py create mode 100644 src/octoprint/plugins/svgtogcode/profiles/default.profile.yaml create mode 100644 src/octoprint/plugins/svgtogcode/static/css/svgtogcode.css create mode 100644 src/octoprint/plugins/svgtogcode/static/js/svgtogcode.js create mode 100644 src/octoprint/plugins/svgtogcode/static/less/svgtogcode.less create mode 100644 src/octoprint/plugins/svgtogcode/templates/svgtogcode_settings_dialog.jinja2 diff --git a/src/octoprint/filemanager/__init__.py b/src/octoprint/filemanager/__init__.py index 6c27752a..ce0c98b0 100644 --- a/src/octoprint/filemanager/__init__.py +++ b/src/octoprint/filemanager/__init__.py @@ -17,7 +17,7 @@ from .storage import LocalFileStorage extensions = dict( # extensions for 3d model files model=dict( - stl=["stl"] + stl=["stl","svg"] ), # extensions for printable machine code machinecode=dict( diff --git a/src/octoprint/plugins/svgtogcode/__init__.py b/src/octoprint/plugins/svgtogcode/__init__.py new file mode 100644 index 00000000..744bdc73 --- /dev/null +++ b/src/octoprint/plugins/svgtogcode/__init__.py @@ -0,0 +1,363 @@ +# coding=utf-8 +from __future__ import absolute_import + +__author__ = "Gina Häußge " +__license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agpl.html" +__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License" + +import logging +import logging.handlers +import os +import flask + +import octoprint.plugin +import octoprint.util +import octoprint.slicing +import octoprint.settings + +default_settings = { + "svgtogcode_engine": None, + "default_profile": None, + "debug_logging": False +} +s = octoprint.plugin.plugin_settings("svgtogcode", defaults=default_settings) + +from .profile import Profile + +blueprint = flask.Blueprint("plugin.svgtogcode", __name__) + +@blueprint.route("/import", methods=["POST"]) +def importSvgToGcodeProfile(): + import datetime + import tempfile + + from octoprint.server import slicingManager + + input_name = "file" + input_upload_name = input_name + "." + s.globalGet(["server", "uploads", "nameSuffix"]) + input_upload_path = input_name + "." + s.globalGet(["server", "uploads", "pathSuffix"]) + + if input_upload_name in flask.request.values and input_upload_path in flask.request.values: + filename = flask.request.values[input_upload_name] + try: + profile_dict = Profile.from_svgtogcode_ini(flask.request.values[input_upload_path]) + except Exception as e: + return flask.make_response("Something went wrong while converting imported profile: {message}".format(e.message), 500) + + elif input_name in flask.request.files: + temp_file = tempfile.NamedTemporaryFile("wb", delete=False) + try: + temp_file.close() + upload = flask.request.files[input_name] + upload.save(temp_file.name) + profile_dict = Profile.from_svgtogcode_ini(temp_file.name) + except Exception as e: + return flask.make_response("Something went wrong while converting imported profile: {message}".format(e.message), 500) + finally: + os.remove(temp_file) + + filename = upload.filename + + else: + return flask.make_response("No file included", 400) + + if profile_dict is None: + return flask.make_response("Could not convert Cura profile", 400) + + name, _ = os.path.splitext(filename) + + # default values for name, display name and description + profile_name = _sanitize_name(name) + profile_display_name = name + profile_description = "Imported from {filename} on {date}".format(filename=filename, date=octoprint.util.getFormattedDateTime(datetime.datetime.now())) + profile_allow_overwrite = False + + # overrides + if "name" in flask.request.values: + profile_name = flask.request.values["name"] + if "displayName" in flask.request.values: + profile_display_name = flask.request.values["displayName"] + if "description" in flask.request.values: + profile_description = flask.request.values["description"] + if "allowOverwrite" in flask.request.values: + from octoprint.server.api import valid_boolean_trues + profile_allow_overwrite = flask.request.values["allowOverwrite"] in valid_boolean_trues + + slicingManager.save_profile("svgtogcode", + profile_name, + profile_dict, + allow_overwrite=profile_allow_overwrite, + display_name=profile_display_name, + description=profile_description) + + result = dict( + resource=flask.url_for("api.slicingGetSlicerProfile", slicer="svgtogcode", name=profile_name, _external=True), + displayName=profile_display_name, + description=profile_description + ) + r = flask.make_response(flask.jsonify(result), 201) + r.headers["Location"] = result["resource"] + return r + + +class SvgToGcodePlugin(octoprint.plugin.SlicerPlugin, + octoprint.plugin.SettingsPlugin, + octoprint.plugin.TemplatePlugin, + octoprint.plugin.AssetPlugin, + octoprint.plugin.BlueprintPlugin, + octoprint.plugin.StartupPlugin): + + def __init__(self): + self._logger = logging.getLogger("octoprint.plugins.svgtogcode") + self._svgtogcode_logger = logging.getLogger("octoprint.plugins.svgtogcode.engine") + + # setup job tracking across threads + import threading + self._slicing_commands = dict() + self._slicing_commands_mutex = threading.Lock() + self._cancelled_jobs = [] + self._cancelled_jobs_mutex = threading.Lock() + + ##~~ StartupPlugin API + + def on_startup(self, host, port): + # setup our custom logger + svgtogcode_logging_handler = logging.handlers.RotatingFileHandler(s.getPluginLogfilePath(postfix="engine"), maxBytes=2*1024*1024) + svgtogcode_logging_handler.setFormatter(logging.Formatter("%(asctime)s %(message)s")) + svgtogcode_logging_handler.setLevel(logging.DEBUG) + + self._svgtogcode_logger.addHandler(svgtogcode_logging_handler) + self._svgtogcode_logger.setLevel(logging.DEBUG if s.getBoolean(["debug_logging"]) else logging.CRITICAL) + self._svgtogcode_logger.propagate = False + + ##~~ BlueprintPlugin API + + def get_blueprint(self): + global blueprint + return blueprint + + ##~~ AssetPlugin API + + def get_asset_folder(self): + import os + return os.path.join(os.path.dirname(os.path.realpath(__file__)), "static") + + def get_assets(self): + return { + "js": ["js/svgtogcode.js"], + "less": ["less/svgtogcode.less"], + "css": ["css/svgtogcode.css"] + } + + ##~~ SettingsPlugin API + + def on_settings_load(self): + return dict( + svgtogcode_engine=s.get(["svgtogcode_engine"]), + default_profile=s.get(["default_profile"]), + debug_logging=s.getBoolean(["debug_logging"]) + ) + + def on_settings_save(self, data): + if "svgtogcode_engine" in data and data["svgtogcode_engine"]: + s.set(["svgtogcode_engine"], data["svgtogcode_engine"]) + if "default_profile" in data and data["default_profile"]: + s.set(["default_profile"], data["default_profile"]) + if "debug_logging" in data: + old_debug_logging = s.getBoolean(["debug_logging"]) + new_debug_logging = data["debug_logging"] in octoprint.settings.valid_boolean_trues + if old_debug_logging != new_debug_logging: + if new_debug_logging: + self._svgtogcode_logger.setLevel(logging.DEBUG) + else: + self._svgtogcode_logger.setLevel(logging.CRITICAL) + s.setBoolean(["debug_logging"], new_debug_logging) + + ##~~ TemplatePlugin API + + def get_template_vars(self): + return dict( + _settings_menu_entry="SvgToGCode" + ) + + def get_template_folder(self): + import os + return os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates") + + ##~~ SlicerPlugin API + + def is_slicer_configured(self): + svgtogcode_engine = s.get(["svgtogcode_engine"]) + # return svgtogcode_engine is not None and os.path.exists(svgtogcode_engine) + return True + + def get_slicer_properties(self): + return dict( + type="svgtogcode", + name="SvgToGCode", + same_device=True + ) + + def get_slicer_default_profile(self): + path = s.get(["default_profile"]) + if not path: + path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "profiles", "default.profile.yaml") + return self.get_slicer_profile(path) + + def get_slicer_profile(self, path): + profile_dict = self._load_profile(path) + + display_name = None + description = None + if "_display_name" in profile_dict: + display_name = profile_dict["_display_name"] + del profile_dict["_display_name"] + if "_description" in profile_dict: + description = profile_dict["_description"] + del profile_dict["_description"] + + properties = self.get_slicer_properties() + return octoprint.slicing.SlicingProfile(properties["type"], "unknown", profile_dict, display_name=display_name, description=description) + + def save_slicer_profile(self, path, profile, allow_overwrite=True, overrides=None): + new_profile = Profile.merge_profile(profile.data, overrides=overrides) + + if profile.display_name is not None: + new_profile["_display_name"] = profile.display_name + if profile.description is not None: + new_profile["_description"] = profile.description + + self._save_profile(path, new_profile, allow_overwrite=allow_overwrite) + + def do_slice(self, model_path, machinecode_path=None, profile_path=None, on_progress=None, on_progress_args=None, on_progress_kwargs=None): + if not profile_path: + profile_path = s.get(["default_profile"]) + if not machinecode_path: + path, _ = os.path.splitext(model_path) + machinecode_path = path + ".gco" + + if on_progress: + if not on_progress_args: + on_progress_args = () + if not on_progress_kwargs: + on_progress_kwargs = dict() + + self._svgtogcode_logger.info("### Slicing %s to %s using profile stored at %s" % (model_path, machinecode_path, profile_path)) + + engine_settings = self._convert_to_engine(profile_path) + + # executable = s.get(["svgtogcode_engine"]) + executable = "/Users/philipp/Documents/dev/MrBeam/mrbeaminkscapeextension/standalone.py" + if not executable: + return False, "Path to CuraEngine is not configured " + + working_dir, _ = os.path.split(executable) + # args = ['"%s"' % executable, '-v', '-p'] + # for k, v in engine_settings.items(): + # args += ["-s", '"%s=%s"' % (k, str(v))] + # args += ['-o', '"%s"' % machinecode_path, '"%s"' % model_path] + + dest_dir, dest_file = os.path.split(machinecode_path) + # args = ['"%s"' % executable, '-f "%s"' % dest_file, '-d "%s"' % dest_dir, '"%s"' % model_path] + args = ['"%s"' % executable, '-f "%s"' % dest_file, '-d "%s"' % dest_dir, '"%s"' % model_path] + + import sarge + command = " ".join(args) + self._logger.info("Running %r in %s" % (command, working_dir)) + try: + p = sarge.run(command, cwd=working_dir, async=True, stdout=sarge.Capture(), stderr=sarge.Capture()) + try: + with self._slicing_commands_mutex: + self._slicing_commands[machinecode_path] = p.commands[0] + + line_seen = False + while p.returncode is None: + line = p.stdout.readline(timeout=0.5) + if not line: + if line_seen: + break + else: + continue + + line_seen = True + self._svgtogcode_logger.debug(line.strip()) + + if on_progress is not None: + pass + finally: + p.close() + + with self._cancelled_jobs_mutex: + if machinecode_path in self._cancelled_jobs: + self._svgtogcode_logger.info("### Cancelled") + raise octoprint.slicing.SlicingCancelled() + + self._svgtogcode_logger.info("### Finished, returncode %d" % p.returncode) + if p.returncode == 0: + return True, None + else: + self._logger.warn("Could not slice via Cura, got return code %r" % p.returncode) + return False, "Got returncode %r" % p.returncode + + except octoprint.slicing.SlicingCancelled as e: + raise e + except: + self._logger.exception("Could not slice via Cura, got an unknown error") + return False, "Unknown error, please consult the log file" + + finally: + with self._cancelled_jobs_mutex: + if machinecode_path in self._cancelled_jobs: + self._cancelled_jobs.remove(machinecode_path) + with self._slicing_commands_mutex: + if machinecode_path in self._slicing_commands: + del self._slicing_commands[machinecode_path] + + self._svgtogcode_logger.info("-" * 40) + + def cancel_slicing(self, machinecode_path): + with self._slicing_commands_mutex: + if machinecode_path in self._slicing_commands: + with self._cancelled_jobs_mutex: + self._cancelled_jobs.append(machinecode_path) + self._slicing_commands[machinecode_path].terminate() + self._logger.info("Cancelled slicing of %s" % machinecode_path) + + def _load_profile(self, path): + import yaml + profile_dict = dict() + with open(path, "r") as f: + try: + profile_dict = yaml.safe_load(f) + except: + raise IOError("Couldn't read profile from {path}".format(path=path)) + return profile_dict + + def _save_profile(self, path, profile, allow_overwrite=True): + if not allow_overwrite and os.path.exists(path): + raise IOError("Cannot overwrite {path}".format(path=path)) + + import yaml + with open(path, "wb") as f: + yaml.safe_dump(profile, f, default_flow_style=False, indent=" ", allow_unicode=True) + + def _convert_to_engine(self, profile_path): + profile = Profile(self._load_profile(profile_path)) + return profile.convert_to_engine() + +def _sanitize_name(name): + if name is None: + return None + + if "/" in name or "\\" in name: + raise ValueError("name must not contain / or \\") + + import string + valid_chars = "-_.() {ascii}{digits}".format(ascii=string.ascii_letters, digits=string.digits) + sanitized_name = ''.join(c for c in name if c in valid_chars) + sanitized_name = sanitized_name.replace(" ", "_") + return sanitized_name.lower() + +__plugin_name__ = "svgtogcode" +__plugin_version__ = "0.1" +__plugin_implementations__ = [SvgToGcodePlugin()] \ No newline at end of file diff --git a/src/octoprint/plugins/svgtogcode/profile.py b/src/octoprint/plugins/svgtogcode/profile.py new file mode 100644 index 00000000..e21d5d94 --- /dev/null +++ b/src/octoprint/plugins/svgtogcode/profile.py @@ -0,0 +1,825 @@ +# coding=utf-8 +from __future__ import absolute_import + +__author__ = "Gina Häußge " +__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' +__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License" + + +from . import s + +import re + +class SupportLocationTypes(object): + NONE = "none" + TOUCHING_BUILDPLATE = "buildplate" + EVERYWHERE = "everywhere" + +class SupportDualTypes(object): + BOTH = "both" + FIRST = "first" + SECOND = "second" + +class SupportTypes(object): + GRID = "grid" + LINES = "lines" + +class PlatformAdhesionTypes(object): + NONE = "none" + BRIM = "brim" + RAFT = "raft" + +class MachineShapeTypes(object): + SQUARE = "square" + CIRCULAR = "circular" + +class GcodeFlavors(object): + REPRAP = "reprap" + REPRAP_VOLUME = "reprap_volume" + ULTIGCODE = "ultigcode" + MAKERBOT = "makerbot" + BFB = "bfb" + MACH3 = "mach3" + + +defaults = dict( + layer_height=0.1, + wall_thickness=0.8, + solid_layer_thickness=0.6, + nozzle_size=0.4, + print_temperature=[220, 0, 0, 0], + print_bed_temperature=70, + platform_adhesion=PlatformAdhesionTypes.NONE, + filament_diameter=[2.85, 0, 0, 0], + filament_flow=100.0, + bottom_thickness=0.3, + first_layer_width_factor=100.0, + object_sink=0.0, + + fill_density=20, + solid_top=True, + solid_bottom=True, + fill_overlap=15, + + # speeds + print_speed=50.0, + travel_speed=150.0, + bottom_layer_speed=20.0, + infill_speed=0.0, + outer_shell_speed=0.0, + inner_shell_speed=0.0, + + # dual extrusion + overlap_dual=0.15, + wipe_tower=False, + wipe_tower_volume=15, + ooze_shield=False, + + # retraction + retraction_enable=True, + retraction_speed=40.0, + retraction_amount=4.5, + retraction_dual_amount=16.5, + retraction_min_travel=1.5, + retraction_combing=True, + retraction_minimal_extrusion=0.02, + retraction_hop=0.0, + + # cooling + cool_min_layer_time=5, + fan_enabled=True, + fan_full_height=0.5, + fan_speed=100, + fan_speed_max=100, + cool_min_feedrate=10, + cool_head_lift=False, + + # support + support=SupportLocationTypes.NONE, + support_type=SupportTypes.GRID, + support_angle=60.0, + support_fill_rate=15, + support_xy_distance=0.7, + support_z_distance=0.15, + support_dual_extrusion=SupportDualTypes.BOTH, + + # platform adhesion + skirt_line_count=1, + skirt_gap=3.0, + skirt_minimal_length=150.0, + brim_line_count=20, + raft_margin=5.0, + raft_line_spacing=3.0, + raft_base_thickness=0.3, + raft_base_linewidth=1.0, + raft_interface_thickness=0.27, + raft_interface_linewidth=0.4, + raft_airgap=0.22, + raft_surface_layers=2, + + # repairing + fix_horrible_union_all_type_a=True, + fix_horrible_union_all_type_b=False, + fix_horrible_use_open_bits=False, + fix_horrible_extensive_stitching=False, + + # extras + spiralize=False, + follow_surface=False, + + machine_width=205, + machine_depth=205, + machine_center_is_zero=False, + has_heated_bed=False, + gcode_flavor=GcodeFlavors.REPRAP, + extruder_amount=1, + steps_per_e=0, + start_gcode=[ + # 1 extruder + """;Sliced at: {day} {date} {time} +;Basic settings: Layer height: {layer_height} Walls: {wall_thickness} Fill: {fill_density} +;M190 S{print_bed_temperature} ;Uncomment to add your own bed temperature line +;M109 S{print_temperature} ;Uncomment to add your own temperature line +G21 ;metric values +G90 ;absolute positioning +M82 ;set extruder to absolute mode +M107 ;start with the fan off + +G28 X0 Y0 ;move X/Y to min endstops +G28 Z0 ;move Z to min endstops + +G1 Z15.0 F{travel_speed} ;move the platform down 15mm + +G92 E0 ;zero the extruded length +G1 F200 E3 ;extrude 3mm of feed stock +G92 E0 ;zero the extruded length again +G1 F{travel_speed} +;Put printing message on LCD screen +M117 Printing... +""", + # 2 extruders + """;Sliced at: {day} {date} {time} +;Basic settings: Layer height: {layer_height} Walls: {wall_thickness} Fill: {fill_density} +;M190 S{print_bed_temperature} ;Uncomment to add your own bed temperature line +;M104 S{print_temperature} ;Uncomment to add your own temperature line +;M109 T1 S{print_temperature2} ;Uncomment to add your own temperature line +;M109 T0 S{print_temperature} ;Uncomment to add your own temperature line +G21 ;metric values +G90 ;absolute positioning +M107 ;start with the fan off + +G28 X0 Y0 ;move X/Y to min endstops +G28 Z0 ;move Z to min endstops + +G1 Z15.0 F{travel_speed} ;move the platform down 15mm + +T1 ;Switch to the 2nd extruder +G92 E0 ;zero the extruded length +G1 F200 E10 ;extrude 10mm of feed stock +G92 E0 ;zero the extruded length again +G1 F200 E-{retraction_dual_amount} + +T0 ;Switch to the first extruder +G92 E0 ;zero the extruded length +G1 F200 E10 ;extrude 10mm of feed stock +G92 E0 ;zero the extruded length again +G1 F{travel_speed} +;Put printing message on LCD screen +M117 Printing... +""", + # 3 extruders + """;Sliced at: {day} {date} {time} +;Basic settings: Layer height: {layer_height} Walls: {wall_thickness} Fill: {fill_density} +;M190 S{print_bed_temperature} ;Uncomment to add your own bed temperature line +;M104 S{print_temperature} ;Uncomment to add your own temperature line +;M109 T1 S{print_temperature2} ;Uncomment to add your own temperature line +;M109 T0 S{print_temperature} ;Uncomment to add your own temperature line +G21 ;metric values +G90 ;absolute positioning +M107 ;start with the fan off + +G28 X0 Y0 ;move X/Y to min endstops +G28 Z0 ;move Z to min endstops + +G1 Z15.0 F{travel_speed} ;move the platform down 15mm + +T2 ;Switch to the 2nd extruder +G92 E0 ;zero the extruded length +G1 F200 E10 ;extrude 10mm of feed stock +G92 E0 ;zero the extruded length again +G1 F200 E-{retraction_dual_amount} + +T1 ;Switch to the 2nd extruder +G92 E0 ;zero the extruded length +G1 F200 E10 ;extrude 10mm of feed stock +G92 E0 ;zero the extruded length again +G1 F200 E-{retraction_dual_amount} + +T0 ;Switch to the first extruder +G92 E0 ;zero the extruded length +G1 F200 E10 ;extrude 10mm of feed stock +G92 E0 ;zero the extruded length again +G1 F{travel_speed} +;Put printing message on LCD screen +M117 Printing... +""", + # 4 extruders + """;Sliced at: {day} {date} {time} +;Basic settings: Layer height: {layer_height} Walls: {wall_thickness} Fill: {fill_density} +;M190 S{print_bed_temperature} ;Uncomment to add your own bed temperature line +;M104 S{print_temperature} ;Uncomment to add your own temperature line +;M109 T2 S{print_temperature2} ;Uncomment to add your own temperature line +;M109 T1 S{print_temperature2} ;Uncomment to add your own temperature line +;M109 T0 S{print_temperature} ;Uncomment to add your own temperature line +G21 ;metric values +G90 ;absolute positioning +M107 ;start with the fan off + +G28 X0 Y0 ;move X/Y to min endstops +G28 Z0 ;move Z to min endstops + +G1 Z15.0 F{travel_speed} ;move the platform down 15mm + +T3 ;Switch to the 4th extruder +G92 E0 ;zero the extruded length +G1 F200 E10 ;extrude 10mm of feed stock +G92 E0 ;zero the extruded length again +G1 F200 E-{retraction_dual_amount} + +T2 ;Switch to the 3th extruder +G92 E0 ;zero the extruded length +G1 F200 E10 ;extrude 10mm of feed stock +G92 E0 ;zero the extruded length again +G1 F200 E-{retraction_dual_amount} + +T1 ;Switch to the 2nd extruder +G92 E0 ;zero the extruded length +G1 F200 E10 ;extrude 10mm of feed stock +G92 E0 ;zero the extruded length again +G1 F200 E-{retraction_dual_amount} + +T0 ;Switch to the first extruder +G92 E0 ;zero the extruded length +G1 F200 E10 ;extrude 10mm of feed stock +G92 E0 ;zero the extruded length again +G1 F{travel_speed} +;Put printing message on LCD screen +M117 Printing... +""" + ], + end_gcode=[ + # 1 extruder + """;End GCode +M104 S0 ;extruder heater off +M140 S0 ;heated bed heater off (if you have it) + +G91 ;relative positioning +G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure +G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more +G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way + +M84 ;steppers off +G90 ;absolute positioning +;{profile_string} +""", + # 2 extruders + """;End GCode +M104 T0 S0 ;extruder heater off +M104 T1 S0 ;extruder heater off +M140 S0 ;heated bed heater off (if you have it) + +G91 ;relative positioning +G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure +G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more +G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way + +M84 ;steppers off +G90 ;absolute positioning +;{profile_string} +""", + # 3 extruders + """;End GCode +M104 T0 S0 ;extruder heater off +M104 T1 S0 ;extruder heater off +M104 T2 S0 ;extruder heater off +M140 S0 ;heated bed heater off (if you have it) + +G91 ;relative positioning +G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure +G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more +G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way + +M84 ;steppers off +G90 ;absolute positioning +;{profile_string} +""", + # 4 extruders + """;End GCode +M104 T0 S0 ;extruder heater off +M104 T1 S0 ;extruder heater off +M104 T2 S0 ;extruder heater off +M104 T3 S0 ;extruder heater off +M140 S0 ;heated bed heater off (if you have it) + +G91 ;relative positioning +G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure +G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more +G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way + +M84 ;steppers off +G90 ;absolute positioning +;{profile_string} +""" + ], + preSwitchExtruder_gcode=""";Switch between the current extruder and the next extruder, when printing with multiple extruders. +;This code is added before the T(n) +""", + postSwitchExtruder_gcode=""";Switch between the current extruder and the next extruder, when printing with multiple extruders. +;This code is added after the T(n) +""" +) + + +class Profile(object): + + regex_extruder_offset = re.compile("extruder_offset_([xy])(\d)") + regex_filament_diameter = re.compile("filament_diameter(\d?)") + regex_print_temperature = re.compile("print_temperature(\d?)") + regex_strip_comments = re.compile(";.*$", flags=re.MULTILINE) + + @classmethod + def from_svgtogcode_ini(cls, path): + import os + if not os.path.exists(path) or not os.path.isfile(path): + return None + + import ConfigParser + config = ConfigParser.ConfigParser() + try: + config.read(path) + except: + return None + + arrayified_options = ["print_temperature", "filament_diameter", "start.gcode", "end.gcode"] + translated_options = dict( + inset0_speed="outer_shell_speed", + insetx_speed="inner_shell_speed", + layer0_width_factor="first_layer_width_factor", + simple_mode="follow_surface", + ) + translated_options["start.gcode"] = "start_gcode" + translated_options["end.gcode"] = "end_gcode" + value_conversions = dict( + platform_adhesion={ + "None": PlatformAdhesionTypes.NONE, + "Brim": PlatformAdhesionTypes.BRIM, + "Raft": PlatformAdhesionTypes.RAFT + }, + support={ + "None": SupportLocationTypes.NONE, + "Touching buildplate": SupportLocationTypes.TOUCHING_BUILDPLATE, + "Everywhere": SupportLocationTypes.EVERYWHERE + }, + support_type={ + "Lines": SupportTypes.LINES, + "Grid": SupportTypes.GRID + }, + support_dual_extrusion={ + "Both": SupportDualTypes.BOTH, + "First extruder": SupportDualTypes.FIRST, + "Second extruder": SupportDualTypes.SECOND + } + ) + + result = dict() + for section in config.sections(): + if not section in ("profile", "alterations"): + continue + + for option in config.options(section): + ignored = False + key = option + + # try to fetch the value in the correct type + try: + value = config.getboolean(section, option) + except: + # no boolean, try int + try: + value = config.getint(section, option) + except: + # no int, try float + try: + value = config.getfloat(section, option) + except: + # no float, use str + value = config.get(section, option) + index = None + + for opt in arrayified_options: + if key.startswith(opt): + if key == opt: + index = 0 + else: + try: + # try to convert the target index, e.g. print_temperature2 => print_temperature[1] + index = int(key[len(opt):]) - 1 + except ValueError: + # ignore entries for which that fails + ignored = True + key = opt + break + if ignored: + continue + + if key in translated_options: + # if the key has to be translated to a new value, do that now + key = translated_options[key] + + if key in value_conversions and value in value_conversions[key]: + value = value_conversions[key][value] + + if index is not None: + # if we have an array to fill, make sure the target array exists and has the right size + if not key in result: + result[key] = [] + if len(result[key]) <= index: + for n in xrange(index - len(result[key]) + 1): + result[key].append(None) + result[key][index] = value + else: + # just set the value if there's no array to fill + result[key] = value + + # merge it with our default settings, the imported profile settings taking precedence + return cls.merge_profile(result) + + + @classmethod + def merge_profile(cls, profile, overrides=None): + import copy + + result = copy.deepcopy(defaults) + for k in result.keys(): + profile_value = None + override_value = None + + if k in profile: + profile_value = profile[k] + if overrides and k in overrides: + override_value = overrides[k] + + if profile_value is None and override_value is None: + # neither override nor profile, no need to handle this key further + continue + + if k in ("start_gcode", "end_gcode"): + # the array fields need some special treatment. Basically something like this: + # + # override_value: [None, "b"] + # profile_value : ["a" , None, "c"] + # default_value : ["d" , "e" , "f", "g"] + # + # should merge to something like this: + # + # ["a" , "b" , "c", "g"] + # + # So override > profile > default, if neither override nor profile value are available + # the default value should just be left as is + + for x in xrange(len(result[k])): + if override_value is not None and x < len(override_value) and override_value[x] is not None: + # we have an override value for this location, so we use it + result[k][x] = override_value[x] + elif profile_value is not None and x < len(profile_value) and profile_value[x] is not None: + # we have a profile value for this location, so we use it + result[k][x] = profile_value[x] + + else: + # just change the result value to the override_value if available, otherwise to the profile_value if + # that is given, else just leave as is + if override_value is not None: + result[k] = override_value + elif profile_value is not None: + result[k] = profile_value + return result + + def __init__(self, profile): + self.profile = profile + + def get(self, key): + if key in self.profile: + return self.profile[key] + elif key in defaults: + return defaults[key] + else: + return None + + def get_int(self, key, default=None): + value = self.get(key) + if value is None: + return default + + try: + return int(value) + except ValueError: + return default + + def get_float(self, key, default=None): + value = self.get(key) + if value is None: + return default + + if isinstance(value, (str, unicode, basestring)): + value = value.replace(",", ".").strip() + + try: + return float(value) + except ValueError: + return default + + def get_boolean(self, key, default=None): + value = self.get(key) + if value is None: + return default + + if isinstance(value, bool): + return value + elif isinstance(value, (str, unicode, basestring)): + return value.lower() == "true" or value.lower() == "yes" or value.lower() == "on" or value == "1" + elif isinstance(value, (int, float)): + return value > 0 + else: + return value == True + + def get_microns(self, key, default=None): + value = self.get_float(key, default=None) + if value is None: + return default + return int(value * 1000) + + def get_gcode_template(self, key): + extruder_count = s.globalGetInt(["printerParameters", "numExtruders"]) + + if key in self.profile: + gcode = self.profile[key] + else: + gcode = defaults[key] + + if key in ("start_gcode", "end_gcode"): + return gcode[extruder_count-1] + else: + return gcode + + def get_machine_extruder_offset(self, extruder, axis): + extruder_offsets = s.globalGet(["printerParameters", "extruderOffsets"]) + if extruder >= len(extruder_offsets): + return 0.0 + if axis.lower() not in ("x", "y"): + return 0.0 + return extruder_offsets[extruder][axis.lower()] + + def get_profile_string(self): + import base64 + import zlib + + import copy + profile = copy.deepcopy(defaults) + profile.update(self.profile) + for key in ("print_temperature", "print_temperature2", "print_temperature3", "print_temperature4", + "filament_diameter", "filament_diameter2", "filament_diameter3", "filament_diameter4"): + profile[key] = self.get(key) + + result = [] + for k, v in profile.items(): + if isinstance(v, (str, unicode)): + result.append("{k}={v}".format(k=k, v=v.encode("utf-8"))) + else: + result.append("{k}={v}".format(k=k, v=v)) + + return base64.b64encode(zlib.compress("\b".join(result), 9)) + + def replaceTagMatch(self, m): + import time + + pre = m.group(1) + tag = m.group(2) + + if tag == 'time': + return pre + time.strftime('%H:%M:%S') + if tag == 'date': + return pre + time.strftime('%d-%m-%Y') + if tag == 'day': + return pre + ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][int(time.strftime('%w'))] + if tag == 'profile_string': + return pre + 'svgtogcode_OCTO_PROFILE_STRING:%s' % (self.get_profile_string()) + + if pre == 'F' and tag == 'max_z_speed': + f = self.get_float("travel_speed") * 60 + elif pre == 'F' and tag in ['print_speed', 'retraction_speed', 'travel_speed', 'bottom_layer_speed', 'cool_min_feedrate']: + f = self.get_float(tag) * 60 + elif self.get(tag): + f = self.get(tag) + else: + return '%s?%s?' % (pre, tag) + + if (f % 1) == 0: + return pre + str(int(f)) + + return pre + str(f) + + def get_gcode(self, key): + extruder_count = s.globalGetInt(["printerParameters", "numExtruders"]) + + prefix = "" + postfix = "" + + if self.get("gcode_flavor") == GcodeFlavors.ULTIGCODE: + if key == "end_gcode": + return "M25 ;Stop reading from this point on.\n;svgtogcode_PROFILE_STRING:%s\n" % (self.get_profile_string()) + return "" + + if key == "start_gcode": + contents = self.get_gcode_template("start_gcode") + + e_steps = self.get_float("steps_per_e") + if e_steps > 0: + prefix += "M92 E{e_steps}\n" % (e_steps) + temp = self.get_float("print_temperature") + + bedTemp = 0 + if self.get_boolean("has_heated_bed"): + bedTemp = self.get_float("print_bed_temperature") + include_bed_temps = bedTemp > 0 and not "{print_bed_temperature}" in Profile.regex_strip_comments.sub("", contents) + + if include_bed_temps: + prefix += "M140 S{print_bed_temperature}\n" + + if temp > 0 and not "{print_temperature}" in Profile.regex_strip_comments.sub("", contents): + if extruder_count > 0: + def temp_line(temp, extruder, template): + t = temp + if extruder > 0: + print_temp = self.get_float("print_temperature%d" % (extruder + 1)) + if print_temp > 0: + t = print_temp + return template.format(extruder=extruder, temp=t) + for n in xrange(1, extruder_count): + prefix += temp_line(temp, n, "M104 T{extruder} S{temp}\n") + for n in xrange(0, extruder_count): + prefix += temp_line(temp, n, "M109 T{extruder} S{temp}\n") + prefix += "T0\n" + else: + prefix += "M109 S{temp}\n".format(temp=temp) + + if include_bed_temps: + prefix += "M190 S{print_bed_temperature}\n".format(bedTemp=bedTemp) + + else: + contents = self.get_gcode_template(key) + + return unicode(prefix + re.sub("(.)\{([^\}]*)\}", self.replaceTagMatch, contents).rstrip() + '\n' + postfix).strip().encode('utf-8') + '\n' + + def calculate_edge_width_and_line_count(self): + wall_thickness = self.get_float("wall_thickness") + nozzle_size = self.get_float("nozzle_size") + + if self.get_boolean("spiralize") or self.get_boolean("follow_surface"): + return wall_thickness, 1 + if wall_thickness < 0.01: + return nozzle_size, 0 + if wall_thickness < nozzle_size: + return wall_thickness, 1 + + edge_width = None + line_count = int(wall_thickness / (nozzle_size - 0.0001)) + if line_count < 1: + edge_width = nozzle_size + line_count = 1 + line_width = wall_thickness / line_count + line_width_alt = wall_thickness / (line_count + 1) + if line_width > nozzle_size * 1.5: + return line_width_alt, line_count + 1 + if not edge_width: + edge_width = line_width + return edge_width, line_count + + def calculate_solid_layer_count(self): + layer_height = self.get_float("layer_height") + solid_thickness = self.get_float("solid_layer_thickness") + if layer_height == 0.0: + return 1 + import math + return int(math.ceil(solid_thickness / (layer_height - 0.0001))) + + def calculate_minimal_extruder_count(self): + extruder_count = s.globalGetInt(["printerParameters", "numExtruders"]) + if extruder_count < 2: + return 1 + if self.get("support") == SupportLocationTypes.NONE: + return 1 + if self.get("support_dual_extrusion") == SupportDualTypes.SECOND: + return 2 + return 1 + + def convert_to_engine(self): + + + settings = { + "--engraving-laser-speed": self.get_microns("layer_height"), + "initialLayerThickness": self.get_microns("bottom_thickness") if self.get_float("bottom_thickness") > 0.0 else self.get_microns("layer_height"), + + } + + for extruder in range(1, extruder_count): + for axis in ("x", "y"): + settings["extruderOffset[{extruder}].{axis}".format(extruder=extruder, axis=axis.upper())] = self.get_machine_extruder_offset(extruder, axis) + + fanFullHeight = self.get_microns("fan_full_height") + settings["fanFullOnLayerNr"] = (fanFullHeight - settings["initialLayerThickness"] - 1) / settings["layerThickness"] + 1 + if settings["fanFullOnLayerNr"] < 0: + settings["fanFullOnLayerNr"] = 0 + + if self.get("support_type") == SupportTypes.LINES: + settings["supportType"] = 1 + + # infill + if self.get_float("fill_density") == 0: + settings["sparseInfillLineDistance"] = -1 + + elif self.get_float("fill_density") == 100: + settings["sparseInfillLineDistance"] = settings["extrusionWidth"] + settings["downSkinCount"] = 10000 + settings["upSkinCount"] = 10000 + + else: + settings["sparseInfillLineDistance"] = int(100 * edge_width * 1000 / self.get_float("fill_density")) + + # brim/raft/skirt + if self.get("platform_adhesion") == PlatformAdhesionTypes.BRIM: + settings["skirtDistance"] = 0 + settings["skirtLineCount"] = self.get_int("brim_line_count") + + elif self.get("platform_adhesion") == PlatformAdhesionTypes.RAFT: + settings["skirtDistance"] = 0 + settings["skirtLineCount"] = 0 + settings["raftMargin"] = self.get_microns("raft_margin") + settings["raftLineSpacing"] = self.get_microns("raft_line_spacing") + settings["raftBaseThickness"] = self.get_microns("raft_base_thickness") + settings["raftBaseLinewidth"] = self.get_microns("raft_base_linewidth") + settings["raftInterfaceThickness"] = self.get_microns("raft_interface_thickness") + settings["raftInterfaceLinewidth"] = self.get_microns("raft_interface_linewidth") + settings["raftInterfaceLineSpacing"] = self.get_microns("raft_interface_linewidth") * 2 + settings["raftAirGapLayer0"] = self.get_microns("raft_airgap") + settings["raftBaseSpeed"] = self.get_int("bottom_layer_speed") + settings["raftFanSpeed"] = 100 + settings["raftSurfaceThickness"] = settings["raftInterfaceThickness"] + settings["raftSurfaceLinewidth"] = int(edge_width * 1000) + settings["raftSurfaceLineSpacing"] = int(edge_width * 1000 * 0.9) + settings["raftSurfaceLayers"] = self.get_int("raft_surface_layers") + settings["raftSurfaceSpeed"] = self.get_int("bottom_layer_speed") + + else: + settings["skirtDistance"] = self.get_microns("skirt_gap") + settings["skirtLineCount"] = self.get_int("skirt_line_count") + settings["skirtMinLength"] = self.get_microns("skirt_minimal_length") + + # fixing + if self.get_boolean("fix_horrible_union_all_type_a"): + settings["fixHorrible"] |= 0x01 + if self.get_boolean("fix_horrible_union_all_type_b"): + settings["fixHorrible"] |= 0x02 + if self.get_boolean("fix_horrible_use_open_bits"): + settings["fixHorrible"] |= 0x10 + if self.get_boolean("fix_horrible_extensive_stitching"): + settings["fixHorrible"] |= 0x04 + + if settings["layerThickness"] <= 0: + settings["layerThickness"] = 1000 + + # gcode flavor + if self.get("gcode_flavor") == GcodeFlavors.ULTIGCODE: + settings["gcodeFlavor"] = 1 + elif self.get("gcode_flavor") == GcodeFlavors.MAKERBOT: + settings["gcodeFlavor"] = 2 + elif self.get("gcode_flavor") == GcodeFlavors.BFB: + settings["gcodeFlavor"] = 3 + elif self.get("gcode_flavor") == GcodeFlavors.MACH3: + settings["gcodeFlavor"] = 4 + elif self.get("gcode_flavor") == GcodeFlavors.REPRAP_VOLUME: + settings["gcodeFlavor"] = 5 + + # extras + if self.get_boolean("spiralize"): + settings["spiralizeMode"] = 1 + if self.get_boolean("follow_surface"): + settings["simpleMode"] = 1 + + # dual extrusion + if self.get_boolean("wipe_tower") and extruder_count > 1: + import math + settings["wipeTowerSize"] = int(math.sqrt(self.get_float("wipe_tower_volume") * 1000 * 1000 * 1000 / settings["layerThickness"])) + if self.get_boolean("ooze_shield"): + settings["enableOozeShield"] = 1 + + return settings diff --git a/src/octoprint/plugins/svgtogcode/profiles/default.profile.yaml b/src/octoprint/plugins/svgtogcode/profiles/default.profile.yaml new file mode 100644 index 00000000..d53fabc6 --- /dev/null +++ b/src/octoprint/plugins/svgtogcode/profiles/default.profile.yaml @@ -0,0 +1,4 @@ +layer_height: 0.2 +print_temperature: +- 220.0 +- 220.0 \ No newline at end of file diff --git a/src/octoprint/plugins/svgtogcode/static/css/svgtogcode.css b/src/octoprint/plugins/svgtogcode/static/css/svgtogcode.css new file mode 100644 index 00000000..1a060503 --- /dev/null +++ b/src/octoprint/plugins/svgtogcode/static/css/svgtogcode.css @@ -0,0 +1 @@ +table th.settings_plugin_svgtogcode_profiles_key,table td.settings_plugin_svgtogcode_profiles_key{text-overflow:ellipsis;text-align:left;width:200px}table th.settings_plugin_svgtogcode_profiles_name,table td.settings_plugin_svgtogcode_profiles_name{text-overflow:ellipsis;text-align:left}table th.settings_plugin_svgtogcode_profiles_actions,table td.settings_plugin_svgtogcode_profiles_actions{text-align:center;width:100px}table th.settings_plugin_svgtogcode_profiles_actions a,table td.settings_plugin_svgtogcode_profiles_actions a{text-decoration:none;color:#000}table th.settings_plugin_svgtogcode_profiles_actions a.disabled,table td.settings_plugin_svgtogcode_profiles_actions a.disabled{color:#ccc;cursor:default} \ No newline at end of file diff --git a/src/octoprint/plugins/svgtogcode/static/js/svgtogcode.js b/src/octoprint/plugins/svgtogcode/static/js/svgtogcode.js new file mode 100644 index 00000000..1dae298d --- /dev/null +++ b/src/octoprint/plugins/svgtogcode/static/js/svgtogcode.js @@ -0,0 +1,191 @@ +$(function() { + function SvgToGcodeViewModel(parameters) { + var self = this; + + self.loginState = parameters[0]; + self.settingsViewModel = parameters[1]; + self.slicingViewModel = parameters[2]; + + self.fileName = ko.observable(); + + self.placeholderName = ko.observable(); + self.placeholderDisplayName = ko.observable(); + self.placeholderDescription = ko.observable(); + + self.profileName = ko.observable(); + self.profileDisplayName = ko.observable(); + self.profileDescription = ko.observable(); + self.profileAllowOverwrite = ko.observable(true); + + self.uploadElement = $("#settings-svgtogcode-import"); + self.uploadButton = $("#settings-svgtogcode-import-start"); + + self.profiles = new ItemListHelper( + "plugin_svgtogcode_profiles", + { + "id": function(a, b) { + if (a["key"].toLocaleLowerCase() < b["key"].toLocaleLowerCase()) return -1; + if (a["key"].toLocaleLowerCase() > b["key"].toLocaleLowerCase()) return 1; + return 0; + }, + "name": function(a, b) { + // sorts ascending + var aName = a.name(); + if (aName === undefined) { + aName = ""; + } + var bName = b.name(); + if (bName === undefined) { + bName = ""; + } + + if (aName.toLocaleLowerCase() < bName.toLocaleLowerCase()) return -1; + if (aName.toLocaleLowerCase() > bName.toLocaleLowerCase()) return 1; + return 0; + } + }, + {}, + "id", + [], + [], + 5 + ); + + self._sanitize = function(name) { + return name.replace(/[^a-zA-Z0-9\-_\.\(\) ]/g, "").replace(/ /g, "_"); + }; + + self.uploadElement.fileupload({ + dataType: "json", + maxNumberOfFiles: 1, + autoUpload: false, + add: function(e, data) { + if (data.files.length == 0) { + return false; + } + + self.fileName(data.files[0].name); + + var name = self.fileName().substr(0, self.fileName().lastIndexOf(".")); + self.placeholderName(self._sanitize(name).toLowerCase()); + self.placeholderDisplayName(name); + self.placeholderDescription("Imported from " + self.fileName() + " on " + formatDate(new Date().getTime() / 1000)); + + self.uploadButton.on("click", function() { + var form = { + allowOverwrite: self.profileAllowOverwrite() + }; + + if (self.profileName() !== undefined) { + form["name"] = self.profileName(); + } + if (self.profileDisplayName() !== undefined) { + form["displayName"] = self.profileDisplayName(); + } + if (self.profileDescription() !== undefined) { + form["description"] = self.profileDescription(); + } + + data.formData = form; + data.submit(); + }); + }, + done: function(e, data) { + self.fileName(undefined); + self.placeholderName(undefined); + self.placeholderDisplayName(undefined); + self.placeholderDescription(undefined); + self.profileName(undefined); + self.profileDisplayName(undefined); + self.profileDescription(undefined); + self.profileAllowOverwrite(true); + + $("#settings-plugin-svgtogcode-import").modal("hide"); + self.requestData(); + self.slicingViewModel.requestData(); + } + }); + + self.removeProfile = function(data) { + if (!data.resource) { + return; + } + + self.profiles.removeItem(function(item) { + return (item.key == data.key); + }); + + $.ajax({ + url: data.resource(), + type: "DELETE", + success: function() { + self.requestData(); + self.slicingViewModel.requestData(); + } + }); + }; + + self.makeProfileDefault = function(data) { + if (!data.resource) { + return; + } + + _.each(self.profiles.items(), function(item) { + item.isdefault(false); + }); + var item = self.profiles.getItem(function(item) { + return item.key == data.key; + }); + if (item !== undefined) { + item.isdefault(true); + } + + $.ajax({ + url: data.resource(), + type: "PATCH", + dataType: "json", + data: JSON.stringify({default: true}), + contentType: "application/json; charset=UTF-8", + success: function() { + self.requestData(); + } + }); + }; + + self.showImportProfileDialog = function() { + $("#settings_plugin_svgtogcode_import").modal("show"); + }; + + self.requestData = function() { + $.ajax({ + url: API_BASEURL + "slicing/svgtogcode/profiles", + type: "GET", + dataType: "json", + success: self.fromResponse + }); + }; + + self.fromResponse = function(data) { + var profiles = []; + _.each(_.keys(data), function(key) { + profiles.push({ + key: key, + name: ko.observable(data[key].displayName), + description: ko.observable(data[key].description), + isdefault: ko.observable(data[key].default), + resource: ko.observable(data[key].resource) + }); + }); + self.profiles.updateItems(profiles); + }; + + self.onBeforeBinding = function () { + self.settings = self.settingsViewModel.settings; + self.requestData(); + }; + + } + + // view model class, parameters for constructor, container to bind to + ADDITIONAL_VIEWMODELS.push([SvgToGcodeViewModel, ["loginStateViewModel", "settingsViewModel", "slicingViewModel"], document.getElementById("settings_plugin_svgtogcode_dialog")]); +}); \ No newline at end of file diff --git a/src/octoprint/plugins/svgtogcode/static/less/svgtogcode.less b/src/octoprint/plugins/svgtogcode/static/less/svgtogcode.less new file mode 100644 index 00000000..002853d5 --- /dev/null +++ b/src/octoprint/plugins/svgtogcode/static/less/svgtogcode.less @@ -0,0 +1,29 @@ +table { + th, td { + &.settings_plugin_svgtogcode_profiles_key { + text-overflow: ellipsis; + text-align: left; + width: 200px; + } + + &.settings_plugin_svgtogcode_profiles_name { + text-overflow: ellipsis; + text-align: left; + } + + &.settings_plugin_svgtogcode_profiles_actions { + text-align: center; + width: 100px; + + a { + text-decoration: none; + color: #000; + + &.disabled { + color: #ccc; + cursor: default; + } + } + } + } +} \ No newline at end of file diff --git a/src/octoprint/plugins/svgtogcode/templates/svgtogcode_settings_dialog.jinja2 b/src/octoprint/plugins/svgtogcode/templates/svgtogcode_settings_dialog.jinja2 new file mode 100644 index 00000000..f49623b3 --- /dev/null +++ b/src/octoprint/plugins/svgtogcode/templates/svgtogcode_settings_dialog.jinja2 @@ -0,0 +1,247 @@ +
+

{{ _('General') }}

+ +
+
+ +
+ +
+
+
+
+ +
+
+
+ +

{{ _('Profiles') }}

+ + + + + + + + + + + + + + + + + +
{{ _('Identifier') }}{{ _('Name') }}{{ _('Actions') }}
+  |  +
+ + + + + + + + + + +
\ No newline at end of file diff --git a/src/octoprint/static/js/app/viewmodels/files.js b/src/octoprint/static/js/app/viewmodels/files.js index e187ea70..c2194ea0 100644 --- a/src/octoprint/static/js/app/viewmodels/files.js +++ b/src/octoprint/static/js/app/viewmodels/files.js @@ -189,6 +189,12 @@ function GcodeFilesViewModel(printerStateViewModel, loginStateViewModel, slicing self.slicing.show(file.origin, file.name); }; + self.convertSVG = function(file) { + if (!file) return; + + self.slicing.show(file.origin, file.name); + }; + self.initSdCard = function() { self._sendSdCommand("init"); }; @@ -235,7 +241,12 @@ function GcodeFilesViewModel(printerStateViewModel, loginStateViewModel, slicing }; self.templateFor = function(data) { - return "files_template_" + data.type; + var extension = data.name.split('.').pop().toLowerCase(); + if (extension == "svg") { + return "files_template_" + data.type + "_svg"; + } else { + return "files_template_" + data.type; + } }; self.getEntryId = function(data) { @@ -265,6 +276,10 @@ function GcodeFilesViewModel(printerStateViewModel, loginStateViewModel, slicing return self.loginState.isUser() && !(self.isPrinting() || self.isPaused()); }; + self.enableSVGConversion = function(data) { + return self.loginState.isUser() && !(self.isPrinting() || self.isPaused()); + }; + self.enableAdditionalData = function(data) { return data["gcodeAnalysis"] || data["prints"] && data["prints"]["last"]; }; diff --git a/src/octoprint/templates/dialogs.jinja2 b/src/octoprint/templates/dialogs.jinja2 index d3b8661f..feaaea7f 100644 --- a/src/octoprint/templates/dialogs.jinja2 +++ b/src/octoprint/templates/dialogs.jinja2 @@ -133,4 +133,41 @@ {{ _('Disable Access Control') }} {{ _('Keep Access Control Enabled') }} + + + \ No newline at end of file diff --git a/src/octoprint/templates/index.jinja2 b/src/octoprint/templates/index.jinja2 index cdf9e7cc..dfe555a6 100644 --- a/src/octoprint/templates/index.jinja2 +++ b/src/octoprint/templates/index.jinja2 @@ -241,6 +241,16 @@ + +