Isometric Test

test

Log in to post a comment.

// Isometric map with houses
// Using modified isometric projection: 1 tile 10x10m = 2:1 (width:height)

// ============================================
// 1. CONFIGURATION AND CONSTANTS
// ============================================

const GRID_SIZE = 10; // min=1 max=20 step=1 Size of one grid tile in meters
const ISO_WIDTH = 2;  // Tile width in isometric projection
const ISO_HEIGHT = 1; // Tile height in isometric projection
const MAP_SIZE = 15;  // min=5 max=30 step=1 Number of tiles along one side of the map
const NUM_BUILDINGS = 15; // min=1 max=50 step=1 Number of buildings to generate
const BUILDING_MARGIN = 1; // Visual margin of building from tile edge

// ============================================
// 2. HELPER FUNCTIONS - COORDINATE CONVERSION
// ============================================

// Convert 3D coordinates (x, y, z) to 2D isometric
// Point [0,0,0] is in the center bottom
// x grows RIGHT upward ↗️
// y grows LEFT upward ↖️
// z grows straight upward ↑
function to_iso(x, y, z) {
    // Offset so [0,0,0] is in the center: subtract half of the map
    const offset_x = MAP_SIZE * GRID_SIZE / 2;
    const offset_y = MAP_SIZE * GRID_SIZE / 2;
    
    const adjusted_x = x - offset_x;
    const adjusted_y = y - offset_y;
    
    // 180° rotation: X right (inverted), Y left (inverted)
    const rotated_x = -adjusted_x;
    const rotated_y = -adjusted_y;
    
    const iso_x = (rotated_x - rotated_y) * ISO_WIDTH / 2;
    const iso_y = (rotated_x + rotated_y) * ISO_HEIGHT / 2 - z;
    return [iso_x, iso_y];
}

// Calculate depth for sorting (the higher, the further)
function get_depth(x, y, z) {
    return x + y; //- z * 0.01; // Z has little effect so buildings overlap correctly
}

// ============================================
// 3. MAP AND HOUSE GENERATION
// ============================================

function generate_map() {
    const buildings = [];
    const used_positions = new Set();
    
    for (let i = 0; i < NUM_BUILDINGS; i++) {
        let x, y, w, h, height;
        let attempts = 0;
        
        // Find free position
        do {
            w = Math.floor(Math.random() * 3) + 1; // Width 1-3 tiles
            h = Math.floor(Math.random() * 3) + 1; // Depth 1-3 tiles
            x = Math.floor(Math.random() * (MAP_SIZE - w));
            y = Math.floor(Math.random() * (MAP_SIZE - h));
            height = Math.floor(Math.random() * 4) + 1.5; // Height 1-4 tiles
            attempts++;
        } while (check_collision(x, y, w, h, used_positions) && attempts < 50);
        
        if (attempts < 50) {
            // Mark position as used
            for (let dx = 0; dx < w; dx++) {
                for (let dy = 0; dy < h; dy++) {
                    used_positions.add(`${x + dx},${y + dy}`);
                }
            }
            
            buildings.push({
                x: x * GRID_SIZE,
                y: y * GRID_SIZE,
                w: w * GRID_SIZE,
                h: h * GRID_SIZE,
                height: height * GRID_SIZE,
                windowSize: (GRID_SIZE/4) + Math.random() * (GRID_SIZE/3)  // Random window size 1-3
            });
        }
    }
    
    return buildings;
}

function check_collision(x, y, w, h, used_positions) {
    // Check with 1 tile spacing around building
    for (let dx = -1; dx <= w; dx++) {
        for (let dy = -1; dy <= h; dy++) {
            if (used_positions.has(`${x + dx},${y + dy}`)) {
                return true;
            }
        }
    }
    return false;
}

// ============================================
// 4. SURFACE GENERATION (for overlap detection)
// ============================================

// Generate surfaces for one building
function generate_building_surfaces(building) {
    const surfaces = [];
    const { x, y, w, h, height } = building;
    const d = get_depth(x + w, y + h, 0);
    
    // Add visual margin
    const vx = x + BUILDING_MARGIN;
    const vy = y + BUILDING_MARGIN;
    const vw = w - 2 * BUILDING_MARGIN;
    const vh = h - 2 * BUILDING_MARGIN;
    
    // Top surface (square from above)
    surfaces.push({
        type: 'top',
        depth: d,
        points: [
            [vx, vy, height],
            [vx + vw, vy, height],
            [vx + vw, vy + vh, height],
            [vx, vy + vh, height]
        ]
    });
    
    // Bottom surface ("grass")
    const padding = 0.01;
    surfaces.push({
        type: 'top',
        depth: d,
        points: [
            [x + padding, y + padding, 0],
            [x + w - padding, y + padding, 0],
            [x + w - padding, y + h, 0],
            [x + padding, y + h, 0]
        ]
    });
    
    // Left wall (along Y axis)
    surfaces.push({
        type: 'left-wall',
        depth: d,
        points: [
            [vx, vy, 0],
            [vx + vw, vy, 0],
            [vx + vw, vy, height],
            [vx, vy, height]
        ]
    });
    
    // Right wall (along X axis)
    surfaces.push({
        type: 'right-wall',
        depth: d,
        points: [
            [vx, vy, 0],
            [vx, vy + vh, 0],
            [vx, vy + vh, height],
            [vx, vy, height]
        ]
    });
    
    return surfaces;
}

// Generate all surfaces from all buildings
function generate_surfaces(buildings) {
    const surfaces = [];
    
    for (const building of buildings) {
        surfaces.push(...generate_building_surfaces(building));
    }
    
    // Sort by depth (furthest = smallest depth)
    surfaces.sort((a, b) => a.depth - b.depth);
    
    return surfaces;
}

// ============================================
// 5. LINE GENERATION
// ============================================

// Generate lines for one building
function generate_building_lines(building) {
    const lines = [];
    const { x, y, w, h, height } = building;
    const depth = get_depth(x + w, y + h, height);
    
    // Add visual margin
    const vx = x + BUILDING_MARGIN;
    const vy = y + BUILDING_MARGIN;
    const vw = w - 2 * BUILDING_MARGIN;
    const vh = h - 2 * BUILDING_MARGIN;
    
    // Top rectangle (4 edges)
    add_line(lines, vx, vy, height, vx + vw, vy, height, depth);
    add_line(lines, vx + vw, vy, height, vx + vw, vy + vh, height, depth);
    add_line(lines, vx + vw, vy + vh, height, vx, vy + vh, height, depth);
    add_line(lines, vx, vy + vh, height, vx, vy, height, depth);
    
    // Vertical edges (3 visible corners)
    add_line(lines, vx, vy, 0, vx, vy, height, depth);           // front left
    add_line(lines, vx + vw, vy, 0, vx + vw, vy, height, depth);   // front right
    add_line(lines, vx, vy + vh, 0, vx, vy + vh, height, depth);   // back left
    
    // Bottom edges (2 visible)
    add_line(lines, vx, vy, 0, vx + vw, vy, 0, depth);    // front
    add_line(lines, vx, vy, 0, vx, vy + vh, 0, depth);    // left
    
    // Windows on right wall (along X axis)
    const win_size = building.windowSize; // Random window size for this building
    
    // Calculate number of columns based on wall width (no margin)
    const num_cols = Math.max(1, Math.floor(vw / 5)); // One column per ~5 units
    const col_width = vw / num_cols;
    
    // Calculate number of rows based on wall height (no margin)
    const num_rows = Math.max(1, Math.floor(height / 5)); // One row per ~5 units
    const row_height = height / num_rows;
    
    // Windows on right wall (along X axis) - place in center of each column
    for (let col = 0; col < num_cols; col++) {
        const col_center = vx + col * col_width + col_width / 2;
        const wx = col_center - win_size / 2; // Center window in column
        
        for (let row = 0; row < num_rows; row++) {
            const row_center = row * row_height + row_height / 2;
            const wz = row_center - win_size / 2; // Center window in row
            
            // Window rectangle (4 lines)
            add_line(lines, wx, vy, wz, wx + win_size, vy, wz, depth);
            add_line(lines, wx + win_size, vy, wz, wx + win_size, vy, wz + win_size, depth);
            add_line(lines, wx + win_size, vy, wz + win_size, wx, vy, wz + win_size, depth);
            add_line(lines, wx, vy, wz + win_size, wx, vy, wz, depth);
        }
    }
    
    // Windows on left wall (along Y axis) - place in center of each column
    const num_cols_left = Math.max(1, Math.floor(vh / 5));
    const col_depth = vh / num_cols_left;
    
    for (let col = 0; col < num_cols_left; col++) {
        const col_center = vy + col * col_depth + col_depth / 2;
        const wy = col_center - win_size / 2; // Center window in column
        
        for (let row = 0; row < num_rows; row++) {
            const row_center = row * row_height + row_height / 2;
            const wz = row_center - win_size / 2; // Center window in row
            
            // Window rectangle (4 lines)
            add_line(lines, vx, wy, wz, vx, wy + win_size, wz, depth);
            add_line(lines, vx, wy + win_size, wz, vx, wy + win_size, wz + win_size, depth);
            add_line(lines, vx, wy + win_size, wz + win_size, vx, wy, wz + win_size, depth);
            add_line(lines, vx, wy, wz + win_size, vx, wy, wz, depth);
        }
    }
    
    return lines;
}

// Generate grid lines
function generate_grid_lines() {
    const lines = [];
    
    for (let i = 0; i <= MAP_SIZE; i++) {
        // Horizontal lines
        add_line(lines, 0, i * GRID_SIZE, 0, MAP_SIZE * GRID_SIZE, i * GRID_SIZE, 0, 1000);
        // Vertical lines
        add_line(lines, i * GRID_SIZE, 0, 0, i * GRID_SIZE, MAP_SIZE * GRID_SIZE, 0, 1000);
    }
    
    return lines;
}

// Generate all lines (buildings + grid)
function generate_lines(buildings) {
    const lines = [];
    
    // Add lines from buildings
    for (const building of buildings) {
        lines.push(...generate_building_lines(building));
    }
    
    // Add grid
    lines.push(...generate_grid_lines());
    
    // Sort by depth (highest depth = drawn first)
    lines.sort((a, b) => b.depth - a.depth);
    
    return lines;
}

function add_line(lines, x1, y1, z1, x2, y2, z2, depth = 0) {
    //const depth = (get_depth(x1, y1, z1) + get_depth(x2, y2, z2)) / 2 + depthOffset;
    lines.push({
        start: [x1, y1, z1],
        end: [x2, y2, z2],
        depth: depth
    });
}

// ============================================
// 6. OVERLAP DETECTION AND LINE CLIPPING
// ============================================

function clip_line_by_surfaces(line, surfaces) {
    let segments = [{ start: line.start, end: line.end }];
    
    // Go through all surfaces with smaller depth (closer to observer)
    for (const surface of surfaces) {
        if (surface.depth >= line.depth) continue;
        
        const newSegments = [];
        for (const segment of segments) {
            const clipped = clip_segment_by_surface(segment, surface);
            newSegments.push(...clipped);
        }
        segments = newSegments;
        
        if (segments.length === 0) break;
    }
    
    return segments;
}

function clip_segment_by_surface(segment, surface) {
    const [x1, y1, z1] = segment.start;
    const [x2, y2, z2] = segment.end;
    
    // Convert to 2D isometric space
    const [sx, sy] = to_iso(x1, y1, z1);
    const [ex, ey] = to_iso(x2, y2, z2);
    
    // Convert surface to 2D
    const poly = surface.points.map(p => to_iso(p[0], p[1], p[2]));
    
    // Find all intersections with polygon
    const intersections = find_line_polygon_intersections(sx, sy, ex, ey, poly);
    
    // Check if points are inside polygon
    const startInside = point_in_polygon(sx, sy, poly);
    const endInside = point_in_polygon(ex, ey, poly);
    
    // If there are no intersections
    if (intersections.length === 0) {
        if (startInside && endInside) {
            return []; // Entire line hidden
        } else {
            return [segment]; // Entire line visible
        }
    }
    
    // Sort intersections by parameter t
    intersections.sort((a, b) => a.t - b.t);
    
    // Create list of all critical points (start, intersections, end)
    const points = [
        { t: 0, inside: startInside }
    ];
    
    for (const inter of intersections) {
        points.push({ t: inter.t, inside: null }); // Intersection = transition
    }
    
    points.push({ t: 1, inside: endInside });
    
    // Go through all segments between critical points
    const resultSegments = [];
    
    for (let i = 0; i < points.length - 1; i++) {
        const p1 = points[i];
        const p2 = points[i + 1];
        
        // Check if segment middle is visible
        const midT = (p1.t + p2.t) / 2;
        const [midX, midY, midZ] = lerp_3d(segment.start, segment.end, midT);
        const [midIsoX, midIsoY] = to_iso(midX, midY, midZ);
        const midInside = point_in_polygon(midIsoX, midIsoY, poly);
        
        // If middle is outside (visible), add segment
        if (!midInside) {
            const [sx1, sy1, sz1] = lerp_3d(segment.start, segment.end, p1.t);
            const [sx2, sy2, sz2] = lerp_3d(segment.start, segment.end, p2.t);
            
            // Add only if it has non-zero length
            if (Math.abs(p2.t - p1.t) > 0.001) {
                resultSegments.push({
                    start: [sx1, sy1, sz1],
                    end: [sx2, sy2, sz2]
                });
            }
        }
    }
    
    return resultSegments;
}

// Find all intersections of line with polygon
function find_line_polygon_intersections(x1, y1, x2, y2, polygon) {
    const intersections = [];
    
    for (let i = 0; i < polygon.length; i++) {
        const j = (i + 1) % polygon.length;
        const [px1, py1] = polygon[i];
        const [px2, py2] = polygon[j];
        
        const intersection = line_intersection_point(x1, y1, x2, y2, px1, py1, px2, py2);
        if (intersection !== null) {
            intersections.push(intersection);
        }
    }
    
    return intersections;
}

// Find intersection of two lines and return parameter t on first line
function line_intersection_point(x1, y1, x2, y2, x3, y3, x4, y4) {
    const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
    if (Math.abs(denom) < 0.0001) return null;
    
    const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom;
    const u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom;
    
    if (t >= 0 && t <= 1 && u >= 0 && u <= 1) {
        return { t: t };
    }
    
    return null;
}

// Linear interpolation between two 3D points
function lerp_3d(start, end, t) {
    return [
        start[0] + (end[0] - start[0]) * t,
        start[1] + (end[1] - start[1]) * t,
        start[2] + (end[2] - start[2]) * t
    ];
}

function point_in_polygon(x, y, polygon) {
    let inside = false;
    for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
        const xi = polygon[i][0], yi = polygon[i][1];
        const xj = polygon[j][0], yj = polygon[j][1];
        
        const intersect = ((yi > y) !== (yj > y))
            && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
        if (intersect) inside = !inside;
    }
    return inside;
}

function line_intersects_polygon(x1, y1, x2, y2, polygon) {
    for (let i = 0; i < polygon.length; i++) {
        const j = (i + 1) % polygon.length;
        const [px1, py1] = polygon[i];
        const [px2, py2] = polygon[j];
        
        if (lines_intersect(x1, y1, x2, y2, px1, py1, px2, py2)) {
            return true;
        }
    }
    return false;
}

function lines_intersect(x1, y1, x2, y2, x3, y3, x4, y4) {
    const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
    if (Math.abs(denom) < 0.0001) return false;
    
    const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom;
    const u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom;
    
    return t >= 0 && t <= 1 && u >= 0 && u <= 1;
}

// ============================================
// 7. MAIN DRAWING FUNCTION
// ============================================

// Global variables to store state between calls
let buildings = null;
let surfaces = [];
let lines = null;
let currentLineIndex = 0;
const t = new Turtle();

buildings = generate_map();
surfaces = generate_surfaces(buildings);
lines = generate_lines(buildings);

// Debug: Display axis directions in isometry
console.log("Axis orientation in isometry:");
console.log("X axis [10,0,0]:", to_iso(10, 0, 0), "→ right upward");
console.log("Y axis [0,10,0]:", to_iso(0, 10, 0), "→ left upward");
console.log("Z axis [0,0,10]:", to_iso(0, 0, 10), "→ straight upward");

lines.sort((a, b) => b.depth - a.depth);
currentLineIndex = 0;

function walk(i) {
    
    // If we have drawn all lines, finish
    if (currentLineIndex >= lines.length) {
        return false;
    }
    
    // Draw current line
    const line = lines[currentLineIndex];
    const segments = clip_line_by_surfaces(line, surfaces);
    
    currentLineIndex++;
    
    // If there are any visible segments, draw them
    for (const segment of segments) {
        const [sx, sy] = to_iso(segment.start[0], segment.start[1], segment.start[2]);
        const [ex, ey] = to_iso(segment.end[0], segment.end[1], segment.end[2]);
        
        t.penup();
        t.goto(sx, sy);
        t.pendown();
        t.goto(ex, ey);
    }
    
    return t;
}

// ============================================
// DEBUG: Display information
// ============================================

// For debugging you can uncomment:
/*
const testBuildings = generate_map();
console.log('Number of buildings:', testBuildings.length);
console.log('Buildings:', testBuildings);
*/