# Copyright (C) 2022 Justin Schuh <code@justinschuh.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.

[gcode_macro print_start]
description: Inserted by slicer at start of print. Rather than using this macro
  directly you should configure your slicer as instructed in the readme.
  Usage: PRINT_START BED=<temp> EXTRUDER=<temp> [CHAMBER=<temp>]
                     [MESH_MIN=<x,y>] [MESH_MAX=<x,y>] [LAYERS=<num>]
                     [NOZZLE_SIZE=<mm>]
gcode:
  {action_respond_info(
    "This file is using an old The PRINT_START format. This print will run "
    "fine, but you should update your slicer config to take advantage of the "
    "phased PRINT_START macros. The slicer documentation is here:\n"
    "https://github.com/jschuh/klipper-macros\x23slicer-configuration"
  )}

  _PRINT_START_PHASE_INIT {rawparams}
  _PRINT_START_PHASE_PREHEAT
  _PRINT_START_PHASE_PROBING
  _PRINT_START_PHASE_EXTRUDER
  _PRINT_START_PHASE_PURGE

[gcode_macro _print_start_phase_init]
description: Inserted by slicer at start of print. Initializes PRINT_START
  phases.
  Usage: See PRINT_START.
gcode:
  CHECK_KM_CONFIG # Need this in case startup errors were missed.
  SET_GCODE_VARIABLE MACRO=_print_end_inner VARIABLE=cancelled VALUE="{False}"
  _KM_APPLY_PRINT_OFFSET RESET=1
  _PRINT_START_PHASE_CHECK PHASE=none
  {% set km = printer["gcode_macro _km_globals"] %}

  {% if not params.BED %}
    {% set dummy = params.__setitem__('BED', params.BED_TEMP|int) %}
  {% endif %}
  {% if not params.EXTRUDER %}
    {% set dummy = params.__setitem__('EXTRUDER', params.EXTRUDER_TEMP|int) %}
  {% endif %}

  # Stash all the params for use by the other phases.
  PRINT_START_SET PRINT_START_PHASE="init" {% for k in params %}{' %s=\"%s\"' %
    (k,params[k]|replace('\\','\\\\')|replace('\'','\\\'')|replace('\"','\\\"'))
  }{% endfor %}

  # Check and propogate the printable bounds.'
  _km_check_and_set_print_bounds

  M107 # Turn off part cooling fan in case it was on.
  CLEAR_PAUSE
  # Kick off the longest preheats in the init.
  M140 S{params.BED}
  {% if params.CHAMBER %}M141 S{params.CHAMBER|int}{% endif %}
  {% if printer.bed_mesh %}BED_MESH_CLEAR{% endif %}
  # Load a saved mesh if configured.
  {% if km.start_try_saved_surface_mesh and printer.bed_mesh %}
    LOAD_SURFACE_MESH
  {% endif %}
  PRINT_START_SET PRINT_START_PHASE="preheat"

[gcode_macro _print_start_phase_preheat]
description: Inserted by slicer at start of print. Handles the bed and chamber
  heating phases and ends when both are stabilized at their target temperatures.
  Usage: See PRINT_START.
gcode:
  _PRINT_START_PHASE_CHECK PHASE=preheat
  {% set print = printer["gcode_macro print_start_set"].print %}
  {% set BED = print.BED|int %}
  {% set EXTRUDER = print.EXTRUDER|int %}
  {% set CHAMBER = print.CHAMBER|default(0)|int %}
  {% set LAYERS = print.LAYERS|default(0)|int %}
  {% set km = printer["gcode_macro _km_globals"] %}
  # The bed started at no more than 0.2C below and 1.0C above the target temp.
  {% set bed_at_target = (BED + 0.4 - printer.heater_bed.temperature) |
                           abs <= 0.6 %}
  PRINT_START_SET BED_AT_TARGET={1 if bed_at_target else 0}
  {% set start_level_bed_at_temp = km.start_level_bed_at_temp and
                                   not printer.bed_mesh.profile_name %}
  {% set actions_at_temp = start_level_bed_at_temp or
                           km.start_quad_gantry_level_at_temp or
                           km.start_z_tilt_adjust_at_temp or
                           (km.start_home_z_at_temp and not bed_at_target) %}
  {% set bed_overshoot = (BED + (km.start_bed_heat_overshoot if
             (BED and not bed_at_target) else 0.0),
           printer.configfile.settings.heater_bed.max_temp ) | min %}

  INIT_LAYER_GCODE LAYERS="{LAYERS}"
  {% if CHAMBER > 0.0 %}
    M141 S{CHAMBER}
  {% endif %}
  # Start bed heating
  M140 S{bed_overshoot}
  {% if actions_at_temp %}
    # If we're going to run a bed level we either have a probing temp specified
    # or we heat the extruder part way to avoid oozing while probing.
    M104 S{km.start_extruder_probing_temp if km.start_extruder_probing_temp > 0
      else (km.start_extruder_preheat_scale * EXTRUDER)|round(0,'ceil')|int}
  {% else %}
    # Honor the extruder probing temp, in case we just want to delay extruder
    # heating until after the bed is ready.
    M104 S{km.start_extruder_probing_temp if km.start_extruder_probing_temp > 0
      else EXTRUDER}
  {% endif %}
  # home all axes
  _KM_PRINT_STATUS ACTION=CHANGE STATUS=homing RESET_STACK=1
  G28
  G90
  {% if BED > 0.0 %}
    _KM_PRINT_STATUS ACTION=CHANGE STATUS=bed_heating RESET_STACK=1
    # Skip this if the bed was already at target when START_PRINT was called.
    {% if not bed_at_target %}
      PARK
      # Overshoot the target a bit.
      M190 S{bed_overshoot}
      G4 P{km.start_bed_heat_delay / 2}
      M190 R{BED} # Settle down after the overshoot.
      G4 P{km.start_bed_heat_delay / 2}
    {% endif %}
  {% endif %}

  {% if CHAMBER > 0.0 %}
    _KM_PRINT_STATUS ACTION=CHANGE STATUS=chamber_heating RESET_STACK=1
    _KM_PARK_IF_NEEDED HEATER="chamber" RANGE=ABOVE
    M191 S{CHAMBER}
  {% endif %}

  {% if BED > 0.0 and bed_at_target%}
    M190 R{BED} # Extra bed stabilization if we skipped it earlier.
  {% endif %}
  PRINT_START_SET PRINT_START_PHASE="probing"

[gcode_macro _print_start_phase_probing]
description: Inserted by slicer at start of print. Performs probing (including
  mesh bed level) and other operations that should be run when the bed and
  chamber are stabilized at their target temps.
  Usage: See PRINT_START.
gcode:
  _PRINT_START_PHASE_CHECK PHASE=probing
  {% set print = printer["gcode_macro print_start_set"].print %}
  {% set km = printer["gcode_macro _km_globals"] %}
  {% set MESH_MIN = print.MESH_MIN|default(None) %}
  {% set MESH_MAX = print.MESH_MAX|default(None) %}
  # Randomize the placement of the print on the bed.
  {% if km.start_random_placement_max > 0 and print.PRINT_MIN and MESH_MIN %}
    {% set PRINT_MIN = print.PRINT_MIN.split(",")|map('float')|list %}
    {% set PRINT_MAX = print.PRINT_MAX.split(",")|map('float')|list %}
    {% set x_offset = (((km.print_max[0] - km.print_min[0] - PRINT_MAX[0] +
         PRINT_MIN[0] - 2 * km.start_random_placement_padding)|int,
         km.start_random_placement_max * 2)|min, 0)|max %}
    {% set y_offset = (((km.print_max[1] - km.print_min[1] - PRINT_MAX[1] +
         PRINT_MIN[1] - 2 * km.start_random_placement_padding)|int,
         km.start_random_placement_max * 2)|min, 0)|max %}
    {% if x_offset > 0 %}
      {% set x_offset = range(x_offset)|random + km.print_min[0] -
                          PRINT_MIN[0] + km.start_random_placement_padding %}
    {% endif %}
    {% if y_offset > 0 %}
      {% set y_offset = range(y_offset)|random + km.print_min[1] -
                          PRINT_MIN[1] + km.start_random_placement_padding %}
    {% endif %}
    # This MESH_MIN/MESH_MAX gets passed to BED_MESH_CALIBRATE below, but the
    # rest of the macros rely on SET_GCODE_OFFSET performing the adjustment.
    {% set MESH_MIN = MESH_MIN.split(",")|map('float')|list %}
    {% set MESH_MAX = MESH_MAX.split(",")|map('float')|list %}
    {% set MESH_MIN_NEW = (MESH_MIN[0] + x_offset, MESH_MIN[1] + y_offset) %}
    {% set MESH_MAX_NEW = (MESH_MAX[0] + x_offset, MESH_MAX[1] + y_offset) %}
    {action_respond_info(
       "Relocating print origin from (%.3f,%.3f) "|format(*MESH_MIN) +
       "to (%.3f,%.3f)"|format(*MESH_MIN_NEW))}
    {% set MESH_MIN = (MESH_MIN_NEW[0], MESH_MIN_NEW[1])|join(',') %}
    {% set MESH_MAX = (MESH_MAX_NEW[0], MESH_MAX_NEW[1])|join(',') %}
    PRINT_START_SET PRINT_OFFSET={"%d,%d" % (x_offset,y_offset)}
  {% endif %}

  {% set EXTRUDER = print.EXTRUDER|int %}
  {% set km = printer["gcode_macro _km_globals"] %}
  {% set start_level_bed_at_temp = km.start_level_bed_at_temp and
                                   not printer.bed_mesh.profile_name %}
  {% set start_home_z_at_temp = km.start_home_z_at_temp and
                                not print.BED_AT_TARGET|int %}
  {% set actions_at_temp = start_level_bed_at_temp or
                           km.start_quad_gantry_level_at_temp or
                           km.start_z_tilt_adjust_at_temp or
                           start_home_z_at_temp %}
  {% if actions_at_temp %}
    {% if km.start_extruder_probing_temp > 0 %}
      _KM_PRINT_STATUS ACTION=CHANGE STATUS=extruder_heating RESET_STACK=1
      _KM_PARK_IF_NEEDED HEATER={printer.toolhead.extruder} RANGE=2
      M109 R{km.start_extruder_probing_temp}
    {% else %}
      M104 S{EXTRUDER} # set the final extruder target temperature
    {% endif %}
    {% if km.start_z_tilt_adjust_at_temp %}
      _KM_PRINT_STATUS ACTION=CHANGE STATUS=calibrating_z RESET_STACK=1
      Z_TILT_ADJUST
    {% endif %}
    {% if km.start_quad_gantry_level_at_temp %}
      _KM_PRINT_STATUS ACTION=CHANGE STATUS=leveling_gantry RESET_STACK=1
      QUAD_GANTRY_LEVEL
    {% endif %}
    {% if start_home_z_at_temp %}
      _KM_PRINT_STATUS ACTION=CHANGE STATUS=homing RESET_STACK=1
      G28 Z # Re-home only the Z axis now that the bed has stabilized.
    {% endif %}
    {% if start_level_bed_at_temp %}
      _KM_PRINT_STATUS ACTION=CHANGE STATUS=meshing RESET_STACK=1
      _km_mesh_if_needed {% if MESH_MIN %} MESH_MIN={MESH_MIN}{% endif
                       %}{% if MESH_MAX %} MESH_MAX={MESH_MAX}{% endif %}
    {% endif %}
    {% if km.start_extruder_probing_temp > 0 %}
      M104 S{EXTRUDER} # set the final extruder target temperature
    {% endif %}
    G4
  {% endif %}
  PRINT_START_SET PRINT_START_PHASE="extruder"

[gcode_macro _print_start_phase_extruder]
description: Inserted by slicer at start of print. Preheats the extruder.
  Usage: See PRINT_START.
gcode:
  _PRINT_START_PHASE_CHECK PHASE=extruder
  {% set print = printer["gcode_macro print_start_set"].print %}
  {% set EXTRUDER = print.EXTRUDER|default(print.EXTRUDER_TEMP)|float %}
  {% set km = printer["gcode_macro _km_globals"] %}

  # Wait for extruder to reach temperature
  {% if EXTRUDER > 0 %}
    _KM_PRINT_STATUS ACTION=CHANGE STATUS=extruder_heating RESET_STACK=1
    _KM_PARK_IF_NEEDED HEATER={printer.toolhead.extruder} RANGE=ABOVE
    M109 S{EXTRUDER}
  {% endif %}
  PRINT_START_SET PRINT_START_PHASE="purge"

[gcode_macro _print_start_phase_purge]
description: Inserted by slicer at start of print. Generates purge lines.
  Usage: See PRINT_START.
gcode:
  _PRINT_START_PHASE_CHECK PHASE=purge
  # Apply the offset for bed placement randomization.
  _KM_APPLY_PRINT_OFFSET
  # apply Z offset for bed surface (just in case it was reset).
  _APPLY_BED_SURFACE_OFFSET
  {% set print = printer["gcode_macro print_start_set"].print %}
  {% set MESH_MIN = print.MESH_MIN|default(None) %}
  {% set MESH_MAX = print.MESH_MAX|default(None) %}
  {% set NOZZLE_SIZE = print.NOZZLE_SIZE|default(print.NOZZLE_SIZE)|
         default(printer.configfile.settings.extruder.nozzle_diameter)|float %}
  {% set km = printer["gcode_macro _km_globals"] %}
  {% if km.start_purge_length > 0.0 and printer.extruder.can_extrude %}
    _KM_PRINT_STATUS ACTION=CHANGE STATUS=purging RESET_STACK=1
    DRAW_PURGE_LINE WIDTH="{NOZZLE_SIZE * 1.25}" HEIGHT="{NOZZLE_SIZE * 0.625
                    }"{% if MESH_MIN %} PRINT_MIN={MESH_MIN}{% endif
                    %}{% if MESH_MAX %} PRINT_MAX={MESH_MAX}{% endif %}
  {% endif %}
  PRINT_START_SET PRINT_START_PHASE="printing"
  _KM_PRINT_STATUS ACTION=CHANGE STATUS=printing RESET_STACK=1

[gcode_macro _km_park_if_needed]
description: Parks the extruder if the current temperature of the supplied
  heater is not within the specified target range.
  Usage: _KM_PARK_IF_NEEDED HEATER=<heater> RANGE=[<percentage>|ABOVE|BELOW]
gcode:
  # This needs to get called as its own macro to get the current temp evaluated.
  {% set HEATER = params.HEATER if params.HEATER in
                    printer.heaters.available_heaters else
                  ("heater_generic " ~ params.HEATER) %}
  {% set RANGE = (params.RANGE|default(1))|string|upper %}

  {% if printer[HEATER].target %}
    {% if RANGE == "ABOVE" %}
      {% if printer[HEATER].temperature < printer[HEATER].target %}
        PARK
      {% endif %}
    {% elif RANGE == "BELOW" %}
      {% if printer[HEATER].temperature > printer[HEATER].target %}
        PARK
      {% endif %}
    {% elif (printer[HEATER].temperature - printer[HEATER].target)|abs >
             (printer[HEATER].target * RANGE|float * 0.01)|abs %}
      PARK
    {% endif %}
  {% endif %}

[gcode_macro _km_apply_print_offset]
variable_offset: []
gcode:
  {% set print = printer["gcode_macro print_start_set"].print %}
  {% if params.RESET|default(0)|int and offset and
        not printer["gcode_macro _km_save_state"].is_ephemeral%}
    {% set PRINT_OFFSET = [offset.pop(0) * -1, offset.pop() * -1] %}
  {% elif print.PRINT_OFFSET and not offset and
          not printer["gcode_macro _km_save_state"].is_ephemeral %}
    {% set PRINT_OFFSET = print.PRINT_OFFSET.split(",")|map('float')|list %}
    {% set dummy = offset.extend(PRINT_OFFSET) %}
  {% endif %}

  {% if PRINT_OFFSET %}
    _KM_SET_GCODE_OFFSET_BASE {"X_ADJUST=%.2f Y_ADJUST=%.2f"|
                                  format(*PRINT_OFFSET)}
  {% endif %}

[gcode_macro _km_mesh_if_needed]
gcode:
  # TODO: Instead of blindly using the loaded mesh we could probe a few key
  # points on the saved grid. If those probes show that the saved grid is no
  # longer in tolerance we could just run BED_MESH_CALIBRATE_FAST anyway.
  {% if not printer.bed_mesh.profile_name %}
    BED_MESH_CALIBRATE_FAST {rawparams}
  {% endif %}

[gcode_macro print_start_set]
description: Inserted by slicer to set values used by PRINT_START.
  Usage: PRINT_START_SET <VARIABLE>=<value>
variable_print: {}
gcode:
  {%for k in params %}
    {% set dummy = print.__setitem__(k|upper, params[k]) %}
  {% endfor %}

[gcode_macro _print_start_phase_check]
gcode:
  {% set print = printer["gcode_macro print_start_set"].print %}
  {% set PRINT_START_PHASE = print.PRINT_START_PHASE|default("none") %}
  {% if PRINT_START_PHASE != params.PHASE %}
    # Reset the phases manually just to be sure.
    SET_GCODE_VARIABLE MACRO=print_start_set VARIABLE=print VALUE="{{}}"
    {% if params.PHASE != 'none' %}
      {% set error = "PRINT_START phase error. Expected '%s' but found '%s'" %
                       (params.PHASE, PRINT_START_PHASE) %}
      # Do the cancel manually if we're not confident it will fire.
      {% if not 'virtual_sdcard' in printer or
            not printer.virtual_sdcard.is_active or 'CANCEL_PRINT' not in
              printer.configfile.settings.virtual_sdcard.on_error_gcode|
                default("")|upper %}
        {action_respond_info(error)}
        CANCEL_PRINT
      {% else %}
        {action_raise_error(error)}
      {% endif %}
    {% else %}
      { action_respond_info("Expected phase '%s' but found '%s'. Resetting."
                             % (params.PHASE, PRINT_START_PHASE)) }
    {% endif %}
  {% endif %}

[gcode_macro print_end]
description: Inserted by slicer at end of print.
  Usage: PRINT_END
gcode:
  _KM_CHECK_IS_PRINTING
  M400
  _PRINT_END_INNER

  {% set km = printer["gcode_macro _km_globals"] %}

  {% if km.start_clear_adjustments_at_end != 0 %}
    RESET_HEATER_SCALING
    RESET_FAN_SCALING
    M220 S100
    M221 S100
  {% endif %}
  _RESET_LAYER_GCODE
  _RESET_VELOCITY_LIMITS
  TURN_OFF_HEATERS
  M107; turn off fan
  {% if printer.bed_mesh %}BED_MESH_CLEAR{% endif %}
  # Park the toolhead and present the bed
  {% if printer.toolhead.homed_axes|lower == "xyz" %}
    PARK Y="{km.start_end_park_y}"
  {% endif %}
  M84 ; disable steppers
  CLEAR_PAUSE
  SET_GCODE_VARIABLE MACRO=print_start_set VARIABLE=print VALUE="{{}}"
  _KM_PRINT_STATUS ACTION=CHANGE STATUS=ready RESET_STACK=1

[gcode_macro _print_end_inner]
variable_cancelled: False
gcode:
  SET_GCODE_VARIABLE MACRO=_print_end_inner VARIABLE=cancelled VALUE="{False}"
  {% set km = printer["gcode_macro _km_globals"] %}
  {% set toolhead = printer.toolhead %}
  {% set origin = printer.gcode_move.homing_origin%}
  {% set max_x = km.print_max[0] - origin.x %}
  {% set max_y = km.print_max[1] - origin.y %}
  {% set max_z = toolhead.axis_maximum.z - origin.z %}

  {% if not cancelled %}
    _KM_PRINT_STATUS ACTION=CHANGE STATUS=completing
  {% endif %}

  {% if printer.extruder.can_extrude %}
    # Wipe if we're not cancelling a paused print.
    {% if not printer.pause_resume.is_paused and not cancelled and
          toolhead.homed_axes|lower == "xyz" %}
      {% set x_safe = (max_x - toolhead.position.x, 2.0)|min %}
      {% set y_safe = (max_y - toolhead.position.y, 2.0)|min %}
      {% set z_safe = (max_z - toolhead.position.z, 2.0)|min %}
      G91
      G0 Z{z_safe} E-1.0 F{km.travel_speed_z * 2} ; move nozzle up
      G0 X{x_safe} Y{y_safe} E-1.0 F{km.travel_speed_xy} ; remove stringing
      # Remove the offset now that we're done.
      _KM_APPLY_PRINT_OFFSET RESET=1
    {% endif %}

    # Small retract to prevent ooze
    G92 E0
    G1 E{"%.2f" % ((0, 2 - km.load_priming_length / 4)|min)} F1200
    M400
  {% endif %}

[gcode_macro _km_check_is_printing]
variable_debug_state: False # Disables print state check for debugging.
description: Throws an error if print is not currently in progress.
gcode:
  {% if not debug_state and
        printer.idle_timeout.state|string != "Printing" and
        not (printer.virtual_sdcard|default({})).is_active|default(False) and
        not printer.pause_resume.is_paused %}
    { action_raise_error("No active print.") }
  {% endif %}

[gcode_macro _km_check_and_set_print_bounds]
description: Validates all print bounds and caches their values.
gcode:
  {% set km = printer["gcode_macro _km_globals"] %}
  {% set print = printer["gcode_macro print_start_set"].print %}

  # Check the mesh bounds.
  {% if print.MESH_MIN %}
    {% set MESH_MIN = print.MESH_MIN.split(",")|map('float')|list %}
    {% set MESH_MAX = print.MESH_MAX.split(",")|map('float')|list %}
    {%if MESH_MIN[0] < km.print_min[0] or MESH_MIN[1] < km.print_min[1] %}
      {action_raise_error("MESH_MIN %s is outside the printable bounds %s" %
                            (MESH_MIN|string, km.print_min|string))}
    {%elif MESH_MAX[0] > km.print_max[0] or MESH_MAX[1] > km.print_max[1] %}
      {action_raise_error("MESH_MAX %s is outside the printable bounds %s" %
                            (MESH_MAX|string, km.print_max|string))}
    {% endif %}
  {% endif %}

  # Find all the model bounds (including any bounds passed in).
  {% set points = [] %}
  {% if print.MODEL_MIN or print.MODEL_MAX %}
    {% set MODEL_MIN = print.MODEL_MIN.split(",")|map('float')|list %}
    {% set MODEL_MAX = print.MODEL_MAX.split(",")|map('float')|list %}
    {% set points = [MODEL_MIN, MODEL_MAX] %}
  {% endif %}
  {% if (printer.exclude_object|default({})).objects %}
    {% set points = printer.exclude_object.objects|selectattr('polygon')|
                      map(attribute='polygon')|sum(start=points) %}
    {% set points_len = points|length %}
    {% if points_len >= 2 %}
      {% set x_coords = (points|map(attribute=0)|sort|list)[0::points_len-1] %}
      {% set y_coords = (points|map(attribute=1)|sort|list)[0::points_len-1] %}
      {% set MODEL_MIN = (x_coords[0],y_coords[0])|map('float')|list %}
      {% set MODEL_MAX = (x_coords[1],y_coords[1])|map('float')|list %}
      PRINT_START_SET MODEL_MIN="{MODEL_MIN|join(',')
                   }" MODEL_MAX="{MODEL_MAX|join(',')}"
    {% endif %}
  {% endif %}

  {% if MODEL_MIN %}
    # Check the model bounds.
    {% if MODEL_MIN[0] < km.print_min[0] or MODEL_MIN[1] < km.print_min[1] %}
      {action_raise_error("MODEL_MIN %s is outside the printable bounds %s" %
                            (MODEL_MIN|string, km.print_min|string))}
    {% elif MODEL_MAX[0] > km.print_max[0] or MODEL_MAX[1] > km.print_max[1] %}
      {action_raise_error("MODEL_MAX %s is outside the printable bounds %s" %
                            (MODEL_MAX|string, km.print_max|string))}
    {% endif %}
  {% endif %}

  # Set the PRINT_LIMITS
  {% if MESH_MIN and MODEL_MIN %}
    PRINT_START_SET PRINT_MIN="{((MODEL_MIN[0],MESH_MIN[0])|min,
                                 (MODEL_MIN[1],MESH_MIN[1])|min)|join(',')
                  }" PRINT_MAX="{((MODEL_MAX[0],MESH_MAX[0])|max,
                                  (MODEL_MAX[1],MESH_MAX[1])|max)|join(',')}"
  {% else %}
    PRINT_START_SET PRINT_MIN="{km.print_min|join(',')
                 }" PRINT_MAX="{km.print_max|join(',')}"
  {% endif %}
