This commit is contained in:
2026-08-04 02:06:32 +02:00
parent 00e9649afb
commit 4f8be3e56c
2 changed files with 14 additions and 27 deletions
+7 -17
View File
@@ -1,12 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, reactive } from 'vue' import { computed, reactive } from 'vue'
import BlockCanvas from './components/BlockCanvas.vue' import BlockCanvas from './components/BlockCanvas.vue'
import { generateShape, summarize, type ShapeMode } from './composables/useShapeGenerator' import { generateShape, summarize } from './composables/useShapeGenerator'
interface FormState { interface FormState {
width: number width: number
height: number height: number
mode: ShapeMode
useSlabs: boolean useSlabs: boolean
useStairs: boolean useStairs: boolean
cellSize: number cellSize: number
@@ -15,7 +14,6 @@ interface FormState {
const form = reactive<FormState>({ const form = reactive<FormState>({
width: 12, width: 12,
height: 8, height: 8,
mode: 'arch',
useSlabs: true, useSlabs: true,
useStairs: true, useStairs: true,
cellSize: 20, cellSize: 20,
@@ -35,7 +33,6 @@ const shape = computed(() => {
return generateShape({ return generateShape({
width, width,
height, height,
mode: form.mode,
useSlabs: form.useSlabs, useSlabs: form.useSlabs,
useStairs: form.useStairs, useStairs: form.useStairs,
}) })
@@ -47,12 +44,13 @@ const stats = computed(() => summarize(shape.value))
<template> <template>
<div class="page"> <div class="page">
<header> <header>
<h1>Minecraft Arch / Ellipse Generator</h1> <h1>Minecraft Arch Generator</h1>
<p class="subtitle"> <p class="subtitle">
Enter a width and height (in blocks) for the wall panel. An arch or Enter a width and height (in blocks) for the wall panel. An arch
oval opening is cut out of it, smoothed with slabs (half-block steps) opening is cut out of it from the ground up, smoothed with slabs
and stairs (beveled corners) instead of a fully jagged hole outline. (half-block steps) and stairs (beveled corners) instead of a fully
The result is the wall blocks you need to place, not the opening. jagged hole outline. The result is the wall blocks you need to
place, not the opening.
</p> </p>
</header> </header>
@@ -68,14 +66,6 @@ const stats = computed(() => summarize(shape.value))
<input type="number" v-model.number="form.height" :min="MIN_SIZE" :max="MAX_SIZE" /> <input type="number" v-model.number="form.height" :min="MIN_SIZE" :max="MAX_SIZE" />
</label> </label>
<label>
Opening shape
<select v-model="form.mode">
<option value="arch">Arch doorway (cut from the ground up)</option>
<option value="oval">Oval window (cut from the middle)</option>
</select>
</label>
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" v-model="form.useSlabs" /> <input type="checkbox" v-model="form.useSlabs" />
Use slabs (half-block smoothing) Use slabs (half-block smoothing)
+7 -10
View File
@@ -1,5 +1,5 @@
// Builds a Minecraft elevation-view (front-facing) block list for a wall // Builds a Minecraft elevation-view (front-facing) block list for a wall
// panel with an arch- or oval-shaped opening cut out of it. // panel with an arch-shaped opening cut out of it, grounded at the bottom.
// //
// Each block is sub-sampled at 4 quarter-points (splitting the block into a // Each block is sub-sampled at 4 quarter-points (splitting the block into a
// 2x2 grid) against the true ellipse boundary, so the resulting shape is // 2x2 grid) against the true ellipse boundary, so the resulting shape is
@@ -41,12 +41,9 @@ export interface Shape {
columns: Column[] columns: Column[]
} }
export type ShapeMode = 'arch' | 'oval'
export interface GenerateOptions { export interface GenerateOptions {
width: number width: number
height: number height: number
mode: ShapeMode
useSlabs: boolean useSlabs: boolean
useStairs: boolean useStairs: boolean
} }
@@ -56,9 +53,9 @@ function insideOpening(x: number, y: number, a: number, b: number): boolean {
return (x / a) ** 2 + (y / b) ** 2 <= 1 return (x / a) ** 2 + (y / b) ** 2 <= 1
} }
/** Continuous y-coordinate of a row's center: arch shapes sit on the ground (y=0 at the bottom edge), oval shapes are centered vertically. */ /** Continuous y-coordinate of a row's center; the arch sits on the ground, so y=0 is the bottom edge of the grid. */
function rowCenterY(r: number, mode: ShapeMode, height: number): number { function rowCenterY(r: number): number {
return mode === 'oval' ? r - (height - 1) / 2 : r + 0.5 return r + 0.5
} }
function classifyCell(tl: boolean, tr: boolean, bl: boolean, br: boolean, useSlabs: boolean, useStairs: boolean): Cell | null { function classifyCell(tl: boolean, tr: boolean, bl: boolean, br: boolean, useSlabs: boolean, useStairs: boolean): Cell | null {
@@ -85,10 +82,10 @@ function classifyCell(tl: boolean, tr: boolean, bl: boolean, br: boolean, useSla
export function generateShape(opts: GenerateOptions): Shape { export function generateShape(opts: GenerateOptions): Shape {
const width = Math.max(1, Math.round(opts.width)) const width = Math.max(1, Math.round(opts.width))
const height = Math.max(1, Math.round(opts.height)) const height = Math.max(1, Math.round(opts.height))
const { mode, useSlabs, useStairs } = opts const { useSlabs, useStairs } = opts
const a = width / 2 const a = width / 2
const b = mode === 'oval' ? height / 2 : height const b = height
const centerOffsetX = (width - 1) / 2 const centerOffsetX = (width - 1) / 2
const useSubSampling = useSlabs || useStairs const useSubSampling = useSlabs || useStairs
@@ -98,7 +95,7 @@ export function generateShape(opts: GenerateOptions): Shape {
const col: Column = [] const col: Column = []
for (let r = 0; r < height; r++) { for (let r = 0; r < height; r++) {
const cy = rowCenterY(r, mode, height) const cy = rowCenterY(r)
if (!useSubSampling) { if (!useSubSampling) {
col.push(insideOpening(cx, cy, a, b) ? null : { type: 'full' }) col.push(insideOpening(cx, cy, a, b) ? null : { type: 'full' })