Skip to content

Instantly share code, notes, and snippets.

@SchizoDuckie
Created January 6, 2026 12:39
Show Gist options
  • Select an option

  • Save SchizoDuckie/1b1d282429c71131e041f58c6e455cb1 to your computer and use it in GitHub Desktop.

Select an option

Save SchizoDuckie/1b1d282429c71131e041f58c6e455cb1 to your computer and use it in GitHub Desktop.
Claude Opus attempts a herringbone tiling pattern in Fusion360 and goes full token vomit
import adsk.core
import math
from .TilePatternBase import TilePattern
class Herringbone(TilePattern):
"""
45° herringbone pattern.
Tiles are rotated ±45° and interlock at their short edges.
The pattern forms a zigzag/chevron arrangement.
Geometry:
- Tile A: rotated +45° (long axis points to upper-right)
- Tile B: rotated -45° (long axis points to upper-left)
- They meet where A's upper-left end touches B's upper-right end
After ±45° rotation, a tile's endpoints (centers of short edges) are:
- +45° tile: ends at ±(h/2)(cos45, sin45) = ±(h·k/2, h·k/2) where k = √2/2
- -45° tile: ends at ±(h/2)(cos45, -sin45) = ±(h·k/2, -h·k/2)
For A and B to meet (A's +end to B's -end):
B_center = A_center + (h·k, 0) + grout_offset
"""
def __init__(self, config, grid):
super().__init__(config, grid)
self.k = math.sqrt(2) / 2 # cos(45°) = sin(45°)
def get_steps(self):
# The repeating unit spans one interlocked pair
# Horizontal: tile A spans h·k, tile B spans h·k, they share a meeting point
# So one pair spans roughly h·k (A's left half) + h·k (B's right half) = h·k total? No...
# Actually the pair centers are offset by (h+g)·k horizontally
# The pattern repeats when we place the next A at B's right end
# Repeat = 2 * (h + g) * k horizontally, same vertically due to symmetry
unit = (self.th + self.tw + 2 * self.grout) * self.k
return unit, unit
def get_max_tile_area(self):
return self.tw * self.th
def use_bbox_filter(self):
# Rotated tiles don't fit in axis-aligned bbox check
return False
def _tile_corners(self, cx, cy, w, h, angle_rad):
"""
Return the 4 corners of a rectangle centered at (cx, cy),
with dimensions w (width) x h (height), rotated by angle_rad.
Returns list of (x, y) tuples in order: BL, BR, TR, TL
"""
cos_a = math.cos(angle_rad)
sin_a = math.sin(angle_rad)
hw, hh = w / 2, h / 2
# Corners in local coords (unrotated), starting bottom-left, going CCW
local_corners = [
(-hw, -hh), # bottom-left
( hw, -hh), # bottom-right
( hw, hh), # top-right
(-hw, hh), # top-left
]
# Rotate each corner around origin, then translate to (cx, cy)
result = []
for lx, ly in local_corners:
rx = lx * cos_a - ly * sin_a
ry = lx * sin_a + ly * cos_a
result.append((cx + rx, cy + ry))
return result
def draw_geometry(self, sketch, progress):
lines = sketch.sketchCurves.sketchLines
grid = self.grid
w = self.tw # tile width (short dimension)
h = self.th # tile height (long dimension)
g = self.grout
k = self.k # √2/2
# Angles
angle_pos = math.radians(45)
angle_neg = math.radians(-45)
# === PAIR GEOMETRY ===
# Tile A at +45°: center at (ax, ay)
# - Upper-left end at: (ax - h·k/2, ay + h·k/2)
# - Lower-right end at: (ax + h·k/2, ay - h·k/2)
#
# Tile B at -45°: center at (bx, by)
# - Upper-right end at: (bx + h·k/2, by + h·k/2)
# - Lower-left end at: (bx - h·k/2, by - h·k/2)
#
# For A's upper-left to meet B's upper-right (forming a ^ chevron):
# A's UL end + grout_vec = B's UR end (relative positions)
# (ax - h·k/2, ay + h·k/2) + grout_vec = (bx + h·k/2, by + h·k/2)
#
# The grout is perpendicular to the meeting edges. Both short edges
# are vertical in local tile coords, so after ±45° rotation:
# - A's UL short edge is rotated +45°, its outward normal points at +135° = (-k, k)
# - B's UR short edge is rotated -45°, its outward normal points at +45° = (k, k)
# The grout gap is along the line connecting the edge centers.
# Since A's UL is at (-h·k/2, +h·k/2) relative to A, and B's UR is at (+h·k/2, +h·k/2) relative to B,
# the gap direction is horizontal (both have same y-offset from their centers).
# So grout_vec ≈ (g, 0) for horizontal separation.
#
# Therefore:
# bx + h·k/2 = ax - h·k/2 + g
# bx = ax - h·k + g
# by + h·k/2 = ay + h·k/2
# by = ay
#
# Wait, that puts B to the LEFT of A. Let me reconsider...
#
# Actually for a TYPICAL herringbone where B is to the right and above:
# A's LOWER-RIGHT end should meet B's LOWER-LEFT end.
# A's LR: (ax + h·k/2, ay - h·k/2)
# B's LL: (bx - h·k/2, by - h·k/2)
# Gap direction: from A's LR outward normal to B's LL
# A's LR short edge normal (outward from tile): at +45° - 90° = -45° direction = (k, -k)
#
# For them to meet with grout g (measured perpendicular to edges):
# A's LR + g·(k, -k) = B's LL... but this gets complicated.
#
# SIMPLER APPROACH: place B so the tile edges are g apart.
# The center-to-center distance when short edges meet with grout g:
# Distance from A center to A's LR end: h/2 (along tile's long axis)
# Distance from B center to B's LL end: h/2 (along tile's long axis)
# The two ends meet with gap g between edges (not centers of ends).
#
# Since A's long axis is at +45° and B's is at -45°, and they meet at ends:
# A's LR end position: A_center + (h/2)·(cos45, sin45) = A_center + (h·k/2, h·k/2)
# Wait, I had this wrong. Let me be very careful.
#
# For +45° rotation, the LOCAL +y axis (tile's length direction) maps to:
# local (0, 1) -> (cos45·0 - sin45·1, sin45·0 + cos45·1) = (-k, k)
# So tile's "top" end (local +y) points to upper-left in world coords.
# Tile's "bottom" end (local -y) points to lower-right.
#
# For -45° rotation:
# local (0, 1) -> (cos(-45)·0 - sin(-45)·1, sin(-45)·0 + cos(-45)·1) = (k, k)
# So tile's "top" end points to upper-right.
# Tile's "bottom" end points to lower-left.
#
# For herringbone with chevrons pointing UP:
# A (+45°): bottom-right end is at A_center + (h/2)·(k, -k) = (ax + h·k/2, ay - h·k/2)
# B (-45°): bottom-left end is at B_center + (h/2)·(-k, -k) = (bx - h·k/2, by - h·k/2)
#
# Hmm wait, let me recalc. For +45°, rotating local point (0, -h/2):
# x' = 0·cos45 - (-h/2)·sin45 = h·k/2
# y' = 0·sin45 + (-h/2)·cos45 = -h·k/2
# So A's bottom end (local -y) is at (ax + h·k/2, ay - h·k/2). Correct.
#
# For -45°, rotating local point (0, -h/2):
# x' = 0·cos(-45) - (-h/2)·sin(-45) = (-h/2)·k = -h·k/2
# y' = 0·sin(-45) + (-h/2)·cos(-45) = -h·k/2
# So B's bottom end is at (bx - h·k/2, by - h·k/2). Correct.
#
# For these ends to be adjacent (with grout), their positions should be:
# A's bottom end + offset = B's bottom end
# where offset accounts for the grout gap perpendicular to the edges.
#
# At the meeting point, A's short edge is perpendicular to A's long axis (+45°),
# so A's short edge is at -45°. Its outward normal (away from A's center) points
# in the direction of the bottom end, which is at +45° from A's center.
# Wait no, the edge normal is perpendicular to the edge, and the edge is the SHORT
# edge at the end of the tile.
#
# The short edge (width w) at the bottom of tile A, after +45° rotation:
# - Edge runs perpendicular to tile's long axis
# - Long axis is at direction (cos45, sin45) after rotation... no wait.
#
# Let me think differently. The short edge at local y = -h/2 spans from
# local (-w/2, -h/2) to (w/2, -h/2). After +45° rotation:
# (-w/2, -h/2) -> ( -w/2·c + h/2·s, -w/2·s - h/2·c ) = ( (-w+h)·k/2, (-w-h)·k/2 )
# ( w/2, -h/2) -> ( w/2·c + h/2·s, w/2·s - h/2·c ) = ( (w+h)·k/2, (w-h)·k/2 )
#
# So the bottom edge of A runs from ( (h-w)·k/2, -(w+h)·k/2 ) to ( (h+w)·k/2, (w-h)·k/2 )
# relative to A's center. This edge is at angle +45° (rises as x increases).
# Its outward normal (pointing away from tile center, i.e., toward -y local, which is
# toward lower-right world) is at angle +45° - 90° = -45°, direction (k, -k).
#
# Similarly, B's bottom edge (local y = -h/2 after -45° rotation):
# (-w/2, -h/2) -> ( -w/2·c - h/2·s, w/2·s - h/2·c ) where c=k, s=-k
# = ( -w·k/2 - h·(-k)/2, -w·(-k)/2 - h·k/2 ) = ( (-w+h)·k/2, (w-h)·k/2 )
# Wait, let me redo with s = sin(-45°) = -k:
# (-w/2, -h/2) -> ( -w/2·k - (-h/2)·(-k), -w/2·(-k) + (-h/2)·k )
# = ( -w·k/2 - h·k/2, w·k/2 - h·k/2 )
# = ( -(w+h)·k/2, (w-h)·k/2 )
# ( w/2, -h/2) -> ( w/2·k - (-h/2)·(-k), w/2·(-k) + (-h/2)·k )
# = ( w·k/2 - h·k/2, -w·k/2 - h·k/2 )
# = ( (w-h)·k/2, -(w+h)·k/2 )
#
# So B's bottom edge runs from ( -(w+h)·k/2, (w-h)·k/2 ) to ( (w-h)·k/2, -(w+h)·k/2 )
# relative to B's center. This edge is at angle -45° (falls as x increases).
# Its outward normal is at -45° - 90° = -135°, direction (-k, -k).
#
# For the two edges to be parallel and facing each other with grout g between:
# A's edge normal is (k, -k), B's edge normal is (-k, -k). These aren't opposite!
# That means these edges aren't parallel. They meet at an angle.
#
# OH. I see. In herringbone, the tiles don't meet edge-to-edge parallel.
# They meet at a CORNER - specifically, the short edge of one meets the
# LONG edge of the other at a corner. That's what creates the zigzag.
#
# Let me reconsider the pattern. In 45° herringbone:
# - Tile A (+45°) has its corner touch Tile B (-45°)'s corner
# - Specifically, A's "right" corner meets B's "left" corner
#
# For +45° tile A, the "rightmost" point (maximum x) is one of the corners.
# Corners after +45° rotation (from local positions):
# BL (-w/2, -h/2) -> ( (-w+h)·k/2, (-w-h)·k/2 ) -- this is bottom in world
# BR ( w/2, -h/2) -> ( (w+h)·k/2, (w-h)·k/2 ) -- this is right in world
# TR ( w/2, h/2) -> ( (w-h)·k/2, (w+h)·k/2 ) -- this is top in world
# TL (-w/2, h/2) -> ( (-w-h)·k/2, (h-w)·k/2 ) -- this is left in world
#
# Rightmost point of A is BR: A_center + ( (w+h)·k/2, (w-h)·k/2 )
#
# For -45° tile B, corners:
# BL (-w/2, -h/2) -> ( -(w+h)·k/2, (w-h)·k/2 ) -- this is left in world
# BR ( w/2, -h/2) -> ( (w-h)·k/2, -(w+h)·k/2 ) -- this is bottom in world
# TR ( w/2, h/2) -> ( (w+h)·k/2, (h-w)·k/2 ) -- this is right in world
# TL (-w/2, h/2) -> ( (h-w)·k/2, (w+h)·k/2 ) -- this is top in world
#
# Leftmost point of B is BL: B_center + ( -(w+h)·k/2, (w-h)·k/2 )
#
# For A's BR corner to meet B's BL corner (with grout):
# A_center + ( (w+h)·k/2, (w-h)·k/2 ) + grout_vec = B_center + ( -(w+h)·k/2, (w-h)·k/2 )
#
# The grout_vec should be perpendicular to the meeting edges and have magnitude g.
# At this corner, A's BR corner is where A's bottom edge meets A's right edge.
# A's right edge (from BR to TR) is at angle... let me compute:
# BR = ( (w+h)·k/2, (w-h)·k/2 ), TR = ( (w-h)·k/2, (w+h)·k/2 )
# Direction BR->TR: ( (w-h)·k/2 - (w+h)·k/2, (w+h)·k/2 - (w-h)·k/2 ) = ( -h·k, h·k )
# This is direction (-1, 1) normalized, i.e., angle 135°.
#
# So A's right edge runs at 135°. B's left edge (from BL to TL):
# BL = ( -(w+h)·k/2, (w-h)·k/2 ), TL = ( (h-w)·k/2, (w+h)·k/2 )
# Direction BL->TL: ( (h-w)·k/2 + (w+h)·k/2, (w+h)·k/2 - (w-h)·k/2 ) = ( h·k, h·k )
# This is direction (1, 1), angle 45°.
#
# So at the meeting point, A's edge is at 135° and B's edge is at 45°.
# They meet at a right angle! The grout fills a small triangular/corner gap.
#
# For simplicity, let's say grout_vec is along the line from A's corner to B's corner.
# Since corners meet (with grout gap g), and the gap is roughly diagonal:
# B_center = A_center + ( (w+h)·k/2, (w-h)·k/2 ) + (g·k, g·k) - ( -(w+h)·k/2, (w-h)·k/2 )
# = A_center + ( (w+h)·k + g·k, g·k )
# = A_center + ( (w + h + g)·k, g·k )
#
# Hmm, but g·k seems too small for the y-offset. Let me think about this visually.
#
# In a proper herringbone, the tiles form rows of chevrons. Each chevron is an A-B pair.
# The next row's chevrons nest into the gaps of the previous row.
#
# Let me try a cleaner approach: define offsets empirically based on where tiles
# need to END UP, then derive the center positions.
#
# For interlocking, after one A-B chevron, the next A should start where B ends.
# B's rightmost point: B_center + ( (w+h)·k/2, (h-w)·k/2 ) [this is B's TR corner]
#
# If the next A's leftmost point should be there (with grout):
# A2's TL corner: A2_center + ( (-w-h)·k/2, (h-w)·k/2 )
# A2_center + ( (-w-h)·k/2, (h-w)·k/2 ) = B_center + ( (w+h)·k/2, (h-w)·k/2 ) + grout
#
# This is getting complex. Let me just use the key relationship:
#
# Offset from A to B (horizontally, same chevron):
# dx_AB = (w + h + g) · k [derived above, roughly]
# dy_AB = g · k [small vertical offset for grout]
#
# But actually from the image, it looks like dy should be larger - the B tile
# is noticeably higher than A. Looking at standard herringbone, within one chevron,
# if A is at +45° and B is at -45°, B is offset both right AND up significantly.
#
# Let me reconsider: the offset should make the tiles INTERLOCK, meaning
# B's corner fits into A's notch.
#
# For A at +45° centered at origin:
# - Right corner (BR) at ( (w+h)·k/2, (w-h)·k/2 )
# - Top corner (TR) at ( (w-h)·k/2, (w+h)·k/2 )
# - The "notch" between BR and TR (the right edge) runs from BR to TR
#
# For B at -45°, its left edge runs from BL to TL:
# - BL at B_center + ( -(w+h)·k/2, (w-h)·k/2 )
# - TL at B_center + ( (h-w)·k/2, (w+h)·k/2 )
#
# For B's left edge to be parallel to and adjacent to A's right edge:
# A's right edge direction: (-1, 1) (from BR toward TR)
# B's left edge direction: ( (h-w) + (w+h), (w+h) - (w-h) )·k/2 = (h, h)·k = (1, 1)
# These are NOT parallel (perpendicular actually).
#
# So in herringbone, adjacent tiles are NOT edge-to-edge parallel. They meet
# at corners at 90° angles. The pattern works because:
# - A's right corner meets B's left corner
# - B's bottom corner meets (next row's) A's top corner
# etc.
#
# FINAL APPROACH: Place B so its leftmost corner is at A's rightmost corner + grout.
# The grout offset is diagonal since the corners meet at an angle.
#
# A's rightmost: ( (w+h)·k/2, (w-h)·k/2 ) relative to A
# B's leftmost: ( -(w+h)·k/2, (w-h)·k/2 ) relative to B
#
# For corner-to-corner with diagonal grout offset (g in both x and y):
# A_corner + (g, 0) = B_corner [if grout is purely horizontal at this joint]
# A + ( (w+h)·k/2, (w-h)·k/2 ) + (g, 0) = B + ( -(w+h)·k/2, (w-h)·k/2 )
# B = A + ( (w+h)·k/2 + g + (w+h)·k/2, 0 )
# B = A + ( (w+h)·k + g, 0 )
#
# So B_center.x = A_center.x + (w + h)·k + g
# B_center.y = A_center.y
#
# That puts B directly to the right of A (same y). But in the visual, B should
# also be HIGHER than A to form the upward-pointing chevron...
#
# Wait, I think I misidentified which corners meet. Let me look at herringbone again.
# In a ^ chevron:
# - A leans right (/)
# - B leans left (\)
# - They meet at the TOP, forming the peak of the ^
#
# A's topmost point (at +45°): TR corner at ( (w-h)·k/2, (w+h)·k/2 )
# But if h > w (which it usually is for rectangular tiles), this has NEGATIVE x.
# So the topmost point is actually TL: ( (-w-h)·k/2, (h-w)·k/2 )
# Hmm, but (h-w)/2 < (w+h)/2, so TR is higher. Let's verify:
# TR y-coord: (w+h)·k/2
# TL y-coord: (h-w)·k/2
# Since w+h > h-w (assuming w > 0), TR is higher. Good.
#
# So A's highest point is TR: A + ( (w-h)·k/2, (w+h)·k/2 )
# For h > w, x-coord is negative, so this point is up and to the LEFT of A's center.
#
# For B at -45°, highest point is TL: B + ( (h-w)·k/2, (w+h)·k/2 )
# For h > w, x-coord is positive, so this is up and to the RIGHT of B's center.
#
# For the chevron peak, A's TR should meet B's TL:
# A + ( (w-h)·k/2, (w+h)·k/2 ) + grout = B + ( (h-w)·k/2, (w+h)·k/2 )
#
# The grout direction: A's top edge (TL to TR) runs at 135°-180° = ... let me recalc.
# TL = ( (-w-h)·k/2, (h-w)·k/2 ), TR = ( (w-h)·k/2, (w+h)·k/2 )
# Direction TL->TR: ( (w-h) + (w+h), (w+h) - (h-w) )·k/2 = ( 2w, 2w )·k/2 = (w·k, w·k)
# This is direction (1, 1), angle 45°.
#
# B's top edge (TL to TR for B):
# TL = ( (h-w)·k/2, (w+h)·k/2 ), TR = ( (w+h)·k/2, (h-w)·k/2 )
# Direction: ( (w+h) - (h-w), (h-w) - (w+h) )·k/2 = ( 2w, -2w )·k/2 = (w·k, -w·k)
# This is direction (1, -1), angle -45°.
#
# At the peak, A's top edge (at 45°) meets B's top edge (at -45°) at 90°.
# Grout fills the corner. For the corners to be separated by g:
# If we offset purely in x: grout = (g, 0)
# A + ( (w-h)·k/2, (w+h)·k/2 ) + (g, 0) = B + ( (h-w)·k/2, (w+h)·k/2 )
# B = A + ( (w-h)·k/2 + g - (h-w)·k/2, 0 )
# B = A + ( (w-h)·k + g, 0 )
# B = A + ( (w - h)·k + g, 0 )
#
# For h > w, (w-h) is negative, so B is to the LEFT of A? That doesn't match
# the typical herringbone visual.
#
# I think the issue is that herringbone tiles typically have the SHORT dimension
# as width and LONG as height, and they're placed so the long axis is mostly
# horizontal after rotation... let me reconsider the rotation direction.
#
# Maybe +45° should make the tile lean left (\) not right (/)?
# Convention: positive angle = counterclockwise rotation.
# A tile with long axis vertical (local +y up), rotated +45° CCW,
# has its long axis pointing to upper-left.
# Visually this is \ shape.
#
# So for / shape (leaning right), we need -45° rotation.
#
# Let me redefine:
# - Tile A at -45° (long axis to upper-right, / shape)
# - Tile B at +45° (long axis to upper-left, \ shape)
# Together they form a ^ chevron.
#
# Let's redo with this convention.
#
# A at -45°:
# TR corner: ( (w+h)·k/2, (h-w)·k/2 ) <- rightmost and near top (if h > w)
# Topmost: TL at ( (h-w)·k/2, (w+h)·k/2 )
#
# B at +45°:
# TL corner: ( (-w-h)·k/2, (h-w)·k/2 ) <- leftmost
# Topmost: TR at ( (w-h)·k/2, (w+h)·k/2 )
#
# For ^ chevron, A's topmost (TL) meets B's topmost (TR):
# A's TL: A + ( (h-w)·k/2, (w+h)·k/2 )
# B's TR: B + ( (w-h)·k/2, (w+h)·k/2 )
#
# For these to meet with horizontal grout:
# A + ( (h-w)·k/2, (w+h)·k/2 ) + (g, 0) = B + ( (w-h)·k/2, (w+h)·k/2 )
# B = A + ( (h-w)·k/2 + g - (w-h)·k/2, 0 )
# B = A + ( (h-w)·k + g, 0 )
#
# For h > w, (h-w) > 0, so B is to the RIGHT of A.
#
# dx_AB = (h - w)·k + g
# dy_AB = 0
#
# Hmm, but this means A and B have the same y-center, which would make the
# chevron symmetric about a horizontal line. Let me verify this matches
# the standard herringbone pattern... actually yes, within one chevron pair,
# the two tiles are at the same height, meeting at the top.
#
# Pattern repeat:
# The next chevron (A', B') should be positioned so A's right side
# meets A's left side of the next column... actually herringbone repeats
# diagonally.
#
# Let me think about the full pattern:
# Row 0: A0-B0 chevron at (x0, y0)
# Row 1: Offset to interlock, A1-B1 at (x0 + offset_x, y0 + offset_y)
#
# The vertical repeat (within a column):
# A's bottom corner is at A + ( -(w+h)·k/2, -(h+w)·k/2 )... wait let me recalc.
#
# For A at -45° (using c=k, s=-k):
# BL (-w/2, -h/2): x' = -w/2·k - (-h/2)·(-k) = -w·k/2 - h·k/2 = -(w+h)·k/2
# y' = -w/2·(-k) + (-h/2)·k = w·k/2 - h·k/2 = (w-h)·k/2
# So BL is at A + ( -(w+h)·k/2, (w-h)·k/2 )
#
# This is the leftmost point of A (at -45°).
# The bottommost point of A is BR: A + ( (w-h)·k/2, -(w+h)·k/2 )
#
# For the next row's chevron to interlock:
# - Next row's A should have its top meet current row's A's bottom, or
# - Next row should be offset so B of row above meets A of row below
#
# Standard herringbone has each row offset by half a unit, creating
# diagonal interlocking.
#
# Vertical step (row to row): distance from one row's baseline to the next.
# The chevron's total height: from bottommost (A's BR or B's BR) to topmost (peak).
# A's BR (bottommost): ( (w-h)·k/2, -(w+h)·k/2 )
# A's TL (topmost): ( (h-w)·k/2, (w+h)·k/2 )
# Height = (w+h)·k/2 - (-(w+h)·k/2) = (w+h)·k
#
# B's bottommost: BR at ( (w-h)·k/2, -(w+h)·k/2 )... wait that's the same y as A's.
# Actually B at +45°:
# BR (w/2, -h/2): x' = w/2·k - (-h/2)·k = (w+h)·k/2
# y' = w/2·k + (-h/2)·k = (w-h)·k/2
# So B's BR is at ( (w+h)·k/2, (w-h)·k/2 ), which has POSITIVE y (for w < h).
#
# B's bottommost is BL: ( (-w-h)·k/2, (h-w)·k/2 )... that's also positive y for h > w.
#
# Actually wait, for B at +45°:
# BL: (-w/2, -h/2) -> ( -w/2·k + h/2·k, -w/2·k - h/2·k ) = ( (h-w)·k/2, -(w+h)·k/2 )
#
# So B's BL is at ( (h-w)·k/2, -(w+h)·k/2 ), with y = -(w+h)·k/2, same as A's BR.
# Good, the bottoms of A and B are at the same level.
#
# So the chevron pair spans from y = -(w+h)·k/2 to y = +(w+h)·k/2, total height (w+h)·k.
# Plus grout between rows: row step = (w + h)·k + g (vertical grout).
#
# But the rows also need horizontal offset for interlocking. Specifically,
# each row is shifted so that the peak of one row aligns with the valley
# (bottom) of the row above.
#
# Horizontal extent of the chevron pair:
# A's leftmost: ( -(w+h)·k/2, (w-h)·k/2 )
# B's rightmost: ( (w+h)·k/2, (h-w)·k/2 )
# A's center at 0, B's center at (h-w)·k + g from A.
# B's rightmost x = A_x + (h-w)·k + g + (w+h)·k/2 = A_x + (h-w)·k + g + (w+h)·k/2
# = A_x + ((h-w) + (w+h)/2)·k + g
# = A_x + ((2h - 2w + w + h) / 2)·k + g
# = A_x + ((3h - w) / 2)·k + g
# This is getting complicated. Let me just use:
# Chevron width = A_leftmost_x to B_rightmost_x
# = (w+h)·k + dx_AB + (w+h)·k/2...
#
# Actually for repeating the pattern, we need:
# - Horizontal repeat: one full chevron width + grout
# - Vertical repeat: chevron height + grout
# - Row stagger: half of horizontal repeat (so rows interlock)
#
# Let me define:
# chevron_height = (w + h) · k
# chevron_width = ... the full width of an A-B pair
#
# For chevron width:
# A at (-45°) centered at (0, 0):
# leftmost x: -(w+h)·k/2
# rightmost x: (w+h)·k/2
# B at (+45°) centered at ( (h-w)·k + g, 0 ):
# leftmost x: (h-w)·k + g - (w+h)·k/2 = ((h-w) - (w+h)/2)·k + g = ((2h-2w-w-h)/2)·k + g = ((h-3w)/2)·k + g
# rightmost x: (h-w)·k + g + (w-h)·k/2 = ((h-w) + (w-h)/2)·k + g = ((2h-2w+w-h)/2)·k + g = ((h-w)/2)·k + g
#
# Wait, B at +45° has:
# rightmost = TR at ( (w-h)·k/2, (w+h)·k/2 ) relative to B
# So absolute rightmost x of B = B_x + (w-h)·k/2 = (h-w)·k + g + (w-h)·k/2
# = (h-w)·k + (w-h)·k/2 + g
# = (h-w)·k·(1 - 1/2) + g
# = (h-w)·k/2 + g
#
# Hmm, for h > w, this is positive but not huge.
#
# Chevron width = B's rightmost x - A's leftmost x
# = ((h-w)·k/2 + g) - (-(w+h)·k/2)
# = (h-w)·k/2 + g + (w+h)·k/2
# = ((h-w) + (w+h))·k/2 + g
# = h·k + g
#
# So chevron_width = h·k + g.
#
# Horizontal repeat (so chevrons tile without gap): chevron_width + grout = h·k + 2g.
# Vertical repeat: chevron_height + grout = (w+h)·k + g.
# Row stagger: half of horizontal repeat = (h·k + 2g) / 2.
#
# BUT looking at real herringbone, the pattern is usually tilted so that
# the chevrons run diagonally, not in horizontal rows. Each "row" is offset
# both horizontally and vertically.
#
# For simplicity, let me use a parallelogram grid:
# - Primary axis: direction along which chevron pairs repeat
# - Secondary axis: offset for alternating rows
#
# Primary step: (horizontal_repeat, 0) to place chevrons side-by-side
# Secondary step: (stagger, vertical_repeat) for the next row
#
# But looking at the image provided, the current code is placing tiles in
# axis-aligned rows (like StackBond), which doesn't work for herringbone.
#
# Let me simplify: just follow the same iteration pattern as StackBond/Hexagon,
# but compute proper offsets for the A-B pair and proper stagger.
# === FINAL IMPLEMENTATION ===
# Based on the analysis:
# - A is at -45°, B is at +45°
# - Within a pair, B is offset from A by: dx = (h - w)·k + g, dy = 0
# - Pattern repeat:
# x-step = h·k + g (chevron width + grout to next chevron)
# y-step = (w + h)·k + g (chevron height + grout)
# - Row stagger: every other row offset by x-step/2 (or similar)
# Wait, I want to double-check the dx_AB formula. Earlier I had:
# dx_AB = (h - w)·k + g
# But I want to verify by computing the gap between A's rightmost and B's leftmost.
#
# A at -45° centered at (0,0):
# rightmost = TR corner = ( (w+h)·k/2, (h-w)·k/2 )
# B at +45° centered at (dx_AB, 0):
# leftmost = TL corner = ( dx_AB + (-w-h)·k/2, (h-w)·k/2 )
#
# For A's rightmost x + grout = B's leftmost x:
# (w+h)·k/2 + g = dx_AB - (w+h)·k/2
# dx_AB = (w+h)·k + g
#
# Hmm, this gives dx_AB = (w+h)·k + g, not (h-w)·k + g.
#
# Let me reconcile. Earlier I computed corner-to-corner at the peak:
# A's TL (at -45°) meets B's TR (at +45°) with grout.
# But here I'm computing A's TR (rightmost) to B's TL (leftmost).
# These are different corner pairs!
#
# In a proper ^ chevron where the tops meet:
# A's TL is the topmost/leftmost of A's upper region
# B's TR is the topmost/rightmost of B's upper region
# They should meet at the peak.
#
# But for the tiles to not overlap, we also need:
# A's right side doesn't overlap B's left side.
# A's rightmost x < B's leftmost x (with grout).
#
# So the constraining gap is whichever is smaller:
# 1) Peak constraint: A_TL + grout = B_TR => dx = (h-w)·k + g
# 2) Side constraint: A_TR + grout = B_TL => dx = (w+h)·k + g
#
# Since (w+h) > (h-w) for w > 0, constraint 2 requires a LARGER offset.
# So if we use dx = (h-w)·k + g from constraint 1, the tiles would overlap
# on the sides!
#
# We need to use the larger offset: dx_AB = (w + h)·k + g.
#
# But then the tiles don't meet at the peak - there's a gap there too.
# Hmm, this means in actual herringbone, there IS a gap at the peak,
# or the tiles meet at edges not corners?
#
# Let me reconsider. In herringbone, tiles of the SAME row don't necessarily
# touch each other. They're offset in a zigzag, and it's the tiles from
# ADJACENT rows that fill in the gaps.
#
# So within one row:
# - Place chevron at (x0, y0)
# - Place chevron at (x0 + repeat_x, y0)
# - These chevrons don't touch each other; there's a gap between them.
# - The gap is filled by chevrons from the row above or below.
#
# This means:
# - Horizontal repeat = full chevron width plus room for interlocking = larger
# - Row stagger fills in the gaps
#
# For proper interlocking herringbone:
# - repeat_x = (w + h)·k + g (full width of one chevron pair with grout)
# - repeat_y = (w + h)·k + g (full height, but this might be different)
# - stagger_x = repeat_x / 2 (half-step offset for alternating rows)
# - stagger_y = ... depends on exact geometry
#
# Actually, herringbone usually has a diagonal repeat structure. Let me
# just implement it and see. I'll use:
# Offset from A to B within a pair
dx_AB = (h - w) * k + g # This is the PEAK meeting distance
# But we also need tiles not to overlap on sides. Let me add a check...
# Actually, since A and B are rotated opposite ways, their side extents
# might not overlap even with the smaller dx. Let me verify:
#
# A at (0, 0), -45°:
# x extent: [-(w+h)·k/2, (w+h)·k/2]
# B at (dx_AB, 0), +45° with dx_AB = (h-w)·k + g:
# B's x extent: [dx_AB - (w+h)·k/2, dx_AB + (w-h)·k/2]
# = [(h-w)·k + g - (w+h)·k/2, (h-w)·k + g + (w-h)·k/2]
# = [((h-w) - (w+h)/2)·k + g, ((h-w) + (w-h)/2)·k + g]
# = [((2h-2w-w-h)/2)·k + g, ((2h-2w+w-h)/2)·k + g]
# = [((h-3w)/2)·k + g, ((h-w)/2)·k + g]
#
# For h = 2w (common tile ratio):
# A's x extent: [-1.5w·k, 1.5w·k]
# B's x extent: [(-w/2)·k + g, (w/2)·k + g]
#
# A's right edge is at 1.5w·k.
# B's left edge is at -0.5w·k + g.
# For no overlap: 1.5w·k <= -0.5w·k + g, i.e., 2w·k <= g.
# With k ≈ 0.707 and typical grout much smaller than 1.4w, this FAILS.
# So tiles WOULD overlap with dx_AB = (h-w)·k + g for h = 2w.
#
# We need dx_AB such that A's rightmost <= B's leftmost:
# (w+h)·k/2 + g <= dx_AB - (w+h)·k/2
# dx_AB >= (w+h)·k + g
#
# So the correct offset is dx_AB = (w + h)·k + g.
#
# Let me redo:
dx_AB = (w + h) * k + g
dy_AB = 0
# But wait, this puts the tile peaks far apart. Let me re-examine.
# With dx_AB = (w+h)·k + g, the x-distance between peaks is:
# A's peak at A + ((h-w)·k/2, (w+h)·k/2)
# B's peak at B + ((w-h)·k/2, (w+h)·k/2) = ((w+h)·k + g + (w-h)·k/2, (w+h)·k/2)
# Peak x-distance = ((w+h)·k + g + (w-h)·k/2) - (h-w)·k/2
# = (w+h)·k + g + (w-h)·k/2 - (h-w)·k/2
# = (w+h)·k + g + (w-h+h-w)·k/2
# = (w+h)·k + g
#
# So the peaks are (w+h)·k + g apart, which is significant (not touching).
# In standard herringbone, the peaks DO touch (with grout). So maybe
# the solution is that A and B in a "pair" are not adjacent horizontally
# but are offset vertically too?
#
# Let me look at herringbone differently. Perhaps what I'm calling a "pair"
# isn't correct. In herringbone:
# - Each tile touches 2-4 other tiles at various points
# - There's no simple A-B "pair" that tiles the plane
# - The pattern is more complex
#
# Let me try a different approach: place tiles on a grid where each grid
# cell contains ONE tile, and alternate the rotation based on position.
#
# Grid approach:
# - Tile (i, j) is at position (i * sx + j * ox, i * sy + j * oy)
# - Rotation alternates: (i + j) % 2 == 0 -> -45°, else -> +45°
# - sx, sy, ox, oy chosen for proper interlocking
#
# For tiles to interlock:
# - Horizontal neighbors (same j, adjacent i) meet at their side edges
# - Vertical neighbors (same i, adjacent j) meet at their end edges
#
# Let me think about horizontally adjacent tiles (same row, adjacent columns):
# - Tile at (i, j) is -45° (assuming (i+j) even)
# - Tile at (i+1, j) is +45°
# - They should meet side-to-side
#
# For -45° tile, right edge runs from BR to TR:
# BR = ((w-h)·k/2, -(w+h)·k/2), TR = ((w+h)·k/2, (h-w)·k/2)
# Edge direction: ((w+h)-(w-h), (h-w)+(w+h))·k/2 = (h, h)·k = k·(1, 1)
# Edge is at angle 45°.
#
# For +45° tile, left edge runs from BL to TL:
# BL = ((h-w)·k/2, -(w+h)·k/2), TL = ((-w-h)·k/2, (h-w)·k/2)
# Edge direction: ((-w-h)-(h-w), (h-w)+(w+h))·k/2 = (-h, h)·k = k·(-1, 1)
# Edge is at angle 135°.
#
# These edges are perpendicular! They can't be placed side-by-side.
# This confirms that horizontally adjacent tiles in herringbone are NOT
# the same-row tiles but offset-row tiles.
#
# OK here's the real insight: in 45° herringbone, the adjacency is DIAGONAL.
# Tiles that share a full edge are diagonally offset, not horizontally or
# vertically.
#
# Let me parameterize by: for tile A at -45° at origin, which tile is
# directly adjacent along A's right edge?
#
# A's right edge: from ((w-h)·k/2, -(w+h)·k/2) to ((w+h)·k/2, (h-w)·k/2)
# Midpoint: (w·k/2, (h-w-w-h)·k/4) = (w·k/2, -w·k/2)... wait let me recalc.
# Midpoint x: ((w-h)/2 + (w+h)/2)·k/2 = (w)·k/2
# Midpoint y: (-(w+h)/2 + (h-w)/2)·k/2 = (-(w+h)+(h-w))·k/4 = (-2w)·k/4 = -w·k/2
# Midpoint of A's right edge: (w·k/2, -w·k/2)
#
# The adjacent tile B should have its left edge aligned with A's right edge.
# For B at +45°, left edge from BL to TL:
# BL = B + ((h-w)·k/2, -(w+h)·k/2)
# TL = B + (-(w+h)·k/2, (h-w)·k/2)
# Midpoint: B + ((h-w-w-h)·k/4, (-(w+h)+(h-w))·k/4) = B + (-w·k/2, -w·k/2)
#
# For A's right edge midpoint + grout = B's left edge midpoint:
# (w·k/2, -w·k/2) + (g·cos(45°+90°), g·sin(45°+90°)) = B + (-w·k/2, -w·k/2)
# Grout normal to A's right edge (which is at 45°) points at 45°-90° = -45°
# Wait, outward from A is toward +x direction at the right edge.
# Edge direction is (1,1), so normal (outward) is (1,-1)/√2... or (1,1) rotated 90° CW = (1,-1)/√2.
# Hmm, let me think. Edge goes from lower-right to upper-left along A's right edge.
# BR to TR direction: (1,1) (approximately).
# Normal pointing outward (away from A's center) would be perpendicular and point right-ish.
# Rotating (1,1) by -90° (CW): (1, -1).
# So outward normal is direction (1,-1)/√2 = (k, -k).
# Grout offset: g · (k, -k).
#
# (w·k/2, -w·k/2) + g·(k, -k) = B + (-w·k/2, -w·k/2)
# B = (w·k/2 + g·k + w·k/2, -w·k/2 - g·k + w·k/2)
# B = (w·k + g·k, -g·k)
# B = ((w + g)·k, -g·k)
#
# So the tile B at +45°, edge-adjacent to A at -45°, is at position ((w+g)·k, -g·k) relative to A.
# This is to the right and slightly below.
#
# Now, what about the tile below A? Call it C.
# A's bottom edge: from BL to BR.
# BL = (-(w+h)·k/2, (w-h)·k/2), BR = ((w-h)·k/2, -(w+h)·k/2)
# Midpoint: ((-(w+h)+(w-h))·k/4, ((w-h)-(w+h))·k/4) = (-h·k/2, -h·k/2)
# Edge direction BL to BR: ((w-h)+(w+h), -(w+h)-(w-h))·k/2 = (w, -w)·k = k·(1, -1)
# Outward normal (away from A, i.e., downward-ish): rotate (1,-1) by -90° = (-1, -1)...
# Actually (1,-1) rotated 90° CW is (-1, -1)/√2 direction? Let me verify:
# (1, -1) rotated by -90°: (1·cos(-90°) - (-1)·sin(-90°), 1·sin(-90°) + (-1)·cos(-90°))
# = (1·0 - (-1)·(-1), 1·(-1) + (-1)·0) = (-1, -1).
# So outward normal at bottom edge is (-k, -k).
# Grout offset: g·(-k, -k).
#
# Tile C below A: if C is at +45° (alternating pattern):
# C's top edge: from TL to TR.
# TL = C + (-(w+h)·k/2, (h-w)·k/2), TR = C + ((w-h)·k/2, (w+h)·k/2)
# Midpoint: C + ((-w-h+w-h)·k/4, (h-w+w+h)·k/4) = C + (-h·k/2, h·k/2)
#
# For A's bottom edge midpoint + grout = C's top edge midpoint:
# (-h·k/2, -h·k/2) + g·(-k, -k) = C + (-h·k/2, h·k/2)
# C = (-h·k/2 - g·k + h·k/2, -h·k/2 - g·k - h·k/2)
# C = (-g·k, -h·k - g·k)
# C = (-g·k, -(h+g)·k)
#
# So C is at (-g·k, -(h+g)·k) relative to A. This is slightly left and significantly down.
#
# Now I can see the pattern forming:
# - A at (0, 0), -45°
# - B at ((w+g)·k, -g·k), +45° (right neighbor)
# - C at (-g·k, -(h+g)·k), +45° (bottom neighbor)
#
# The pattern doesn't form simple rows. It's a diagonal lattice.
#
# To implement, I'll iterate over a skewed grid:
# - For integers i, j, place a tile at position:
# (i * ux + j * vx, i * uy + j * vy)
# - where (ux, uy) and (vx, vy) are the lattice vectors.
#
# From A to B: (ux, uy) = ((w+g)·k, -g·k)
# From A to C: (vx, vy) = (-g·k, -(h+g)·k)
#
# Rotation: for tile at (i, j), rotation is -45° if (i+j) even, +45° if odd.
#
# Let's implement this.
# Lattice vectors
ux = (w + g) * k
uy = -g * k
vx = -g * k
vy = -(h + g) * k
# Determine grid bounds (how many i, j values to cover the wall)
# We need to cover from (min_x - margin, min_y - margin) to (max_x + margin, max_y + margin)
margin = max(w, h) * 2
# Solve for i, j range:
# x = i * ux + j * vx
# y = i * uy + j * vy
# We need to find i, j such that (x, y) covers the bounding box.
#
# This is a 2D lattice inversion problem. For simplicity, I'll iterate over
# a large rectangular range of i, j and filter by bounding box.
# Estimate range needed:
# For x: i * ux + j * vx ∈ [min_x - margin, max_x + margin]
# ux ≈ w·k (positive), vx ≈ -g·k (negative, small)
# For y: i * uy + j * vy ∈ [min_y - margin, max_y + margin]
# uy ≈ -g·k (negative, small), vy ≈ -h·k (negative)
# Rough estimates:
# i ranges: mainly controls x (since ux >> vx)
# j ranges: mainly controls y (since vy >> uy)
x_extent = grid.max_x - grid.min_x + 2 * margin
y_extent = grid.max_y - grid.min_y + 2 * margin
# Number of i steps to cover x: x_extent / |ux|
# Number of j steps to cover y: y_extent / |vy|
i_count = int(x_extent / abs(ux)) + 10 # Add padding
j_count = int(y_extent / abs(vy)) + 10
# Starting position (shift to cover min corner)
# Solve: i0 * ux + j0 * vx = min_x - margin
# i0 * uy + j0 * vy = max_y + margin (start from top)
#
# Approximate: i0 ≈ (min_x - margin) / ux, j0 ≈ (max_y + margin) / vy
i_start = int((grid.min_x - margin) / ux) - 5
j_start = int((grid.max_y + margin) / vy) - 5 # vy is negative, so this is negative j
count = 0
row = 0
for j in range(j_start, j_start + j_count):
for i in range(i_start, i_start + i_count):
# Tile position
tx = grid.start_x + i * ux + j * vx
ty = grid.start_y + i * uy + j * vy
# Rotation: alternate based on i + j
angle = math.radians(-45) if (i + j) % 2 == 0 else math.radians(45)
# Get corners and draw
pts = self._tile_corners(tx, ty, w, h, angle)
self.draw_polygon(lines, pts)
count += 1
row += 1
progress.message = f"Drawing row {row} ({count} tiles)"
adsk.doEvents()
if progress.wasCancelled:
return True
return False
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment