The following code uses a backtracking technique that gets very slow with increasing size. It precludes long edges, but doesn't check for four tile corners. It attempts to achieve an even distribution of tile areas.
A very stripped down version of Zentra's Tk code is used to display the result and the tile list and placement is also printed.
Update: various bugs fixed and a constant added to control maximum allowed edge length as a fraction of width or height.
Update: the code doesn't scale as poorly as I expected with larger areas to be paved. I guess because it doesn't actually have to do much back tracking until fitting the last tiles so the last couple of "rows" may get relaid multiple times, but hardly ever more than that. The kMaxFrac constant I added can be set to much smaller values than the 0.5 criteria provided by BrowserUK for a maximum edge of half the width or height. In fact it could probably be set in terms of the GCD and be independent of the patio size.
use warnings; use strict; use List::Compare; use Storable qw(dclone); use Tk; use constant kMaxFrac => 0.75; my @colours = ( '#ff8080', '#ff0000', '#8080ff', '#e08080', '#ffffff', '#80ffff', '#ffff80', '#e06080', '#808080', ); my @sizes = ( [300, 300], [450, 300], [450, 450], [600, 450], [600, 600], [600, 300], [750, 600], [750, 750], [900, 900] ); my %lengths; $lengths{$_->[0]}++, $lengths{$_->[1]}++ for @sizes; my @factors = map {[factors ($_)]} keys %lengths; my @commonFactors = findCommon (@factors); my $gcd = 1; my $type = 1; $gcd *= $_ for @commonFactors; @sizes = map {[@$_, $type++]} @sizes; my @rectangles = grep {$_->[0] != $_->[1]} @sizes; my @tiles = (@sizes, map {[$_->[1], $_->[0], $_->[2]]} @rectangles); @tiles = sort {$a->[0] <=> $b->[0]} map {[$_->[0] / $gcd, $_->[1] / $g +cd, $_->[2]]} @tiles; while (<DATA>) { my ($pw, $ph) = map {$_ * 1000} split /\s/; $_ = $gcd * int (($_ + $gcd - 1) / $gcd) for ($pw, $ph); print "Rounded dimensions are $pw mm x $ph mm\n"; $_ /= $gcd for ($pw, $ph); my $area = $pw * $ph; my $nextTileCode = 0; my @stockList; my @corners = [0, 0]; my @tileList; my @row = (0) x $pw; my @patio; for my $tile (@tiles) { my ($x, $y) = @$tile; my $count = ($area / ($x * $y)); $count /= 2 if $x != $y; push @stockList, ($nextTileCode++) x $count; } push @patio, [@row] for 1 .. $ph; if (! placeTiles (0, 0, \@stockList, \@tileList, \@patio)) { print "Didn't find a suitable placement pattern.\n"; next; } # Display the result my $mw = MainWindow->new; my $topframe = $mw->Frame(-bg=>'black')->pack(-fill=>'x'); $_ *= $gcd / 1000 for ($pw, $ph); $topframe->Label(-text=>"X-Y = $pw - $ph (rounded)", -bg => 'black', -fg => 'lightblue', )->pack(-side=>'left'); print "X,\tY,\tType\n"; my $S_canvas = $mw->Scrolled('Canvas', -width => $pw * 100, -height => $ph * 100 + 10, -bg => 'black', -borderwidth => 3, -relief => 'sunken', -scrollbars => 'osoe', )->pack(-expand => 1, -fill =>'both'); my $canvas = $S_canvas->Subwidget('scrolled'); for (@tileList) { $_ *= $gcd for ($_->[0], $_->[1], $_->[3], $_->[4]); }; for (@tileList) { my ($x, $y, $type) = @$_; my ($w, $h, $sizeType) = @{$tiles[$type]}; $_ *= $gcd for ($w, $h); print "$x,\t$y,\t$sizeType ($w x $h)\n" ; $_ /= 10 for ($x, $y, $w, $h); $canvas->createRectangle( $x, $y, $x + $w, $y + $h, -fill => $colours[$sizes[$sizeType - 1][2] - 1], -tags => ['paver'], ); } MainLoop; } sub placeTiles { my ($x, $y, $stockList, $tileList, $patio) = @_; my $height = @$patio; my $width = @{$patio->[0]}; my @availTiles = @$stockList; my $tile; TILE: while (@availTiles) { # Select a tile $tile = $availTiles[rand @availTiles]; # Remove selected tile type from available list @availTiles = grep {$_ != $tile} @availTiles; # Try placing the tile my ($tw, $th) = @{$tiles[$tile]}[0..1]; push @$tileList, [$x, $y, $tile, $tw, $th]; next if $x + $tw > $width; next if $y + $th > $height; next if ! placeTile ($tileList, $patio); next if badEdge ($width, $height, $tileList, $patio); my $newX = $x; my $newY = $y; # Skip to next possible block insertion point if ($newX + $tileList->[-1][3] == $width) { # Move down a row $newX = 0; ++$newY; } else { $newX += $tileList->[-1][3]; } # Scan for next block insertion point SEEK: while ($newY < $height) { while ($newX < $width) { last SEEK if ! $patio->[$newY][$newX]; ++$newX; } $newX = 0; ++$newY; } if ($newY == $height) { next if badEdge ($width, $height, $tileList, $patio); return 1; # Placed last tile, done } # Fill remaining area. Done if ok return 1 if placeTiles ($newX, $newY, $stockList, $tileList, $ +patio); } continue { # Failed, back off and try again unplaceTile ($tileList, $patio); pop @$tileList; } return undef; } sub badEdge { my ($width, $height, $tileList, $patio) = @_; # Check for long horiz edges my @testTiles = sort {$a->[1] <=> $b->[1] || $a->[0] <=> $b->[0]} +@$tileList; my $lastEnd = 0; my $edge = 0; for my $tile (@testTiles) { next if $tile->[3] == $width; # Allow a full width block next if $tile->[1] == 0; # Skip blocks on the edge if ($tile->[0] != $lastEnd) { $edge = $tile->[3]; } else { $edge += $tile->[3]; } if ($edge > $width * kMaxFrac) { return 1; } } continue { $lastEnd = $tile->[0] + $tile->[3]; } # Check for long vertical edges @testTiles = sort {$a->[0] <=> $b->[0] || $a->[1] <=> $b->[1]} @$t +ileList; $lastEnd = 0; $edge = 0; for my $tile (@testTiles) { next if $tile->[4] == $height; # Allow a full heigth block next if $tile->[0] == 0; # Skip blocks on the edge if ($tile->[1] != $lastEnd || $tile->[1] < $lastEnd) { $edge = $tile->[4]; } else { $edge += $tile->[4]; } if ($edge > $height * kMaxFrac) { return 1; } } continue { $lastEnd = $tile->[1] + $tile->[4]; } return undef; } sub placeTile { my ($tileList, $patio) = @_; my ($x, $y, $tile, $w, $h) = @{$tileList->[-1]}; my $yEnd = $y + $h - 1; my $xEnd = $x + $w - 1; if ($yEnd > $#$patio || $xEnd > $#{$patio->[0]}) { return undef; } # Check for collisions for my $yi ($y .. $yEnd) { for my $xi ($x .. $xEnd) { if ($patio->[$yi][$xi]) { return undef ; } } } # Paint the tile for my $yi ($y .. $yEnd) { ++$patio->[$yi][$_] for $x .. $xEnd; } return 1; } sub unplaceTile { my ($tileList, $patio) = @_; my ($x, $y, $tile, $w, $h) = @{$tileList->[-1]}; if (! $patio->[$y][$x]) { return; } my $yEnd = $y + $h - 1; my $xEnd = $x + $w - 1; if ($yEnd > $#$patio || $xEnd > $#{$patio->[0]}) { return undef; } for my $yi ($y .. $yEnd) { for my $xi ($x .. $xEnd) { --$patio->[$yi][$xi] if $patio->[$yi][$xi]; } } } { my @p; BEGIN {@p = (2, 3, 5, 7);} sub nextprime { my $p = $p[$#p]; DIV: while (1) { $p += 2; my $pc = 0; while (1) { my $d = $p[$pc++]; last if $d * $d > $p; next DIV unless $p % $d; } $p[@p] = $p; return $p; } } sub factors { my $n = shift; my $pc = 0; my @result; while ($n > 1) { my $d = $p[$pc++] || nextprime(); if ($d * $d > $n) { push @result, $n; $n = 1; } else { while ($n % $d == 0) { $n /= $d; push @result, $d; } } } return @result; } } sub findCommon { my @lists = @_; my @common; while (@lists == grep {@$_} @lists) { my %hits; $hits{$_->[0]}++ for @lists; my $least = (sort keys %hits)[0]; push @common, $least if $hits{$least} == @lists; $_->[0] eq $least && shift @$_ for @lists; } return @common; } __DATA__ 1.5 1.2
Sample output:
Rounded dimensions are 1500 mm x 1200 mm X, Y, Type 0, 0, 1 (300 x 300) 300, 0, 3 (450 x 450) 750, 0, 2 (300 x 450) 1050, 0, 2 (450 x 300) 0, 300, 1 (300 x 300) 1050, 300, 3 (450 x 450) 300, 450, 2 (300 x 450) 600, 450, 2 (450 x 300) 0, 600, 1 (300 x 300) 600, 750, 2 (300 x 450) 900, 750, 4 (600 x 450) 0, 900, 6 (600 x 300)
In reply to Re: Challenge: 2D random layout of variable-sized rectangular units.
by GrandFather
in thread Challenge: 2D random layout of variable-sized rectangular units.
by BrowserUk
| For: | Use: | ||
| & | & | ||
| < | < | ||
| > | > | ||
| [ | [ | ||
| ] | ] |