Skip to content

Instantly share code, notes, and snippets.

@tana
Last active May 26, 2026 14:50
Show Gist options
  • Select an option

  • Save tana/fabd2c195c4f421c9adc3ad27850ed58 to your computer and use it in GitHub Desktop.

Select an option

Save tana/fabd2c195c4f421c9adc3ad27850ed58 to your computer and use it in GitHub Desktop.
G-code replicator

Requirements

The slicer needs to be configured to emit EXCLUDE_OBJECT_(DEFINE|START|END) commands. Also the printer has to support Firmware Retraction (G10 and G11 commands).

Usage

python replicate.py COUNT_X COUNT_Y SPACING_X SPACING_Y MOVE_Z
  • COUNT_X: X-axis replication count
  • COUNT_Y: Y-axis replication count
  • SPACING_X: X-axis spacing between object centers (millimeters)
  • SPACING_Y: Y-axis spacing between object centers (millimeters)
  • MOVE_Z: Z-axis clearance during move between objects
MIT License
Copyright (c) 2026 tana_ash
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# G-code replicator
# Written by tana_ash. May 2026.
# Licensed under the MIT License.
import sys
import re
import itertools
filename = sys.argv[1]
count_x = int(sys.argv[2])
count_y = int(sys.argv[3])
spacing_x = int(sys.argv[4])
spacing_y = int(sys.argv[5])
move_z = int(sys.argv[6])
exclude_object_define_p = re.compile(r'^EXCLUDE_OBJECT_DEFINE NAME=(?P<name>[\w.]+) CENTER=(?P<center>[0-9.]+,[0-9.]+)( POLYGON=(?P<polygon>\[(\[[0-9.]+,[0-9]+\])(,\[[0-9.]+,[0-9.]+\])*\]))?', re.IGNORECASE)
exclude_object_start_p = re.compile(r'^EXCLUDE_OBJECT_START NAME=(?P<name>[\w.]+)', re.IGNORECASE)
exclude_object_end_p = re.compile(r'^EXCLUDE_OBJECT_END', re.IGNORECASE)
g90_p = re.compile(r'^G90', re.IGNORECASE)
g91_p = re.compile(r'^G91', re.IGNORECASE)
object_defs = dict()
current_object_name = None
stored_gcode = []
relative = False
last_shift_x = 0
last_shift_y = 0
for line in open(filename).readlines():
line = line.rstrip()
match = exclude_object_define_p.match(line)
if match:
name = match.group('name')
center = [float(coord) for coord in match.group('center').split(",")]
polygon = match.group('polygon')
object_defs[name] = (center, polygon)
print(f"Object {name} defined", file=sys.stderr)
for i in range(count_x * count_y):
new_name = name + f"_{i}"
cmd = f"EXCLUDE_OBJECT_DEFINE NAME={new_name} CENTER={center[0]},{center[1]}"
# TODO: polygon coordinate conversion
if polygon:
cmd += f" POLYGON={polygon}"
print(cmd)
continue
match = exclude_object_start_p.match(line)
if match:
name = match.group('name')
if name in object_defs:
current_object_name = name
else:
print(f"Object {name} not found", file=sys.stderr)
continue
match = exclude_object_end_p.match(line)
if match:
if current_object_name is None:
print("Unexpected EXCLUDE_OBJECT_END", file=sys.stderr)
(center_x, center_y), _ = object_defs[current_object_name]
for i, j in itertools.product(range(count_x), range(count_y)):
shift_x = spacing_x * (i - (count_x - 1) / 2)
shift_y = spacing_y * (j - (count_x - 1) / 2)
print(f"EXCLUDE_OBJECT_START NAME={current_object_name}_{count_y * i + j}")
# Retract
print("G10")
# Raise nozzle
print("G91")
print(f"G0 Z{move_z}")
# Move to the current object center
print("G90")
print(f"G0 X{center_x} Y{center_y}")
# Move to the new object center
print(f"G91")
print(f"G0 X{shift_x - last_shift_x} Y{shift_y - last_shift_y}")
# Shift coordinate
print(f"G92 X{center_x} Y{center_y}")
# Lower nozzle
#print("G91")
#print(f"G0 Z{-move_z}")
# Restore relative/absolute mode
if relative:
print("G91")
else:
print("G90")
# Unretract
print("G11")
last_shift_x = shift_x
last_shift_y = shift_y
for stored_line in stored_gcode:
print(stored_line)
print(f"EXCLUDE_OBJECT_END")
current_object_name = None
stored_gcode = []
continue
match = g90_p.match(line)
if match:
print("Changed to absolute mode", file=sys.stderr)
relative = False
match = g91_p.match(line)
if match:
print("Changed to relative mode", file=sys.stderr)
relative = True
if current_object_name:
stored_gcode.append(line)
continue
print(line)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment