Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut elit tellus, luctus nec ullamcorper mattis, pulvinar dapibus leo.
import React, { useMemo, useRef, useState } from "react";
import { Canvas, useFrame } from "@react-three/fiber";
import { OrbitControls, Environment, ContactShadows, Html } from "@react-three/drei";
import * as THREE from "three";
import { Card, CardContent } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Slider } from "@/components/ui/slider";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
const DEG = Math.PI / 180;
const TILT_DEG = 8;
const colorOptions = [
{ name: "Homok", value: "#d6bd8f" },
{ name: "Bézs", value: "#e7d8bd" },
{ name: "Terrakotta", value: "#b86f4f" },
{ name: "Szürke", value: "#9ca3af" },
{ name: "Antracit", value: "#30343b" },
{ name: "Olíva", value: "#8a9361" },
];
function cmToM(v) {
return Number(v || 0) / 100;
}
function fmt(v, digits = 1) {
return Number.isFinite(v) ? v.toFixed(digits) : "–";
}
function vec(x, y, z) {
return new THREE.Vector3(x, y, z);
}
function getPostBases(widthCm, depthCm) {
const w = cmToM(widthCm) / 2;
const d = cmToM(depthCm) / 2;
return {
M1: vec(-w, 0, -d),
M2: vec(w, 0, -d),
A2: vec(w, 0, d),
A1: vec(-w, 0, d),
};
}
function computeModel(params) {
const bases = getPostBases(params.widthCm, params.depthCm);
const heights = {
M1: cmToM(params.M1),
M2: cmToM(params.M2),
A2: cmToM(params.A2),
A1: cmToM(params.A1),
};
const cornerOffsets = {
M1: cmToM(params.offsetM1),
M2: cmToM(params.offsetM2),
A2: cmToM(params.offsetA2),
A1: cmToM(params.offsetA1),
};
const tilt = TILT_DEG * DEG;
const topPoints = {};
const textileCorners = {};
["M1", "M2", "A2", "A1"].forEach((key) => {
const base = bases[key];
const outward = vec(base.x, 0, base.z).normalize();
const top = base.clone().add(outward.multiplyScalar(Math.tan(tilt) * heights[key]));
top.y = heights[key];
topPoints[key] = top;
const inward = bases[key].clone().multiplyScalar(-1).normalize();
textileCorners[key] = top.clone().add(inward.multiplyScalar(cornerOffsets[key]));
});
const order = ["M1", "M2", "A2", "A1"];
const corners = order.map((k) => textileCorners[k]);
return { bases, topPoints, textileCorners, corners, order };
}
function edgeLength(a, b) {
return a.distanceTo(b) * 100;
}
function inwardNormal2D(a, b, center) {
const mid = a.clone().add(b).multiplyScalar(0.5);
const edge = b.clone().sub(a);
const n1 = vec(-edge.z, 0, edge.x).normalize();
const n2 = n1.clone().multiplyScalar(-1);
return mid.clone().add(n1).distanceTo(center) < mid.clone().add(n2).distanceTo(center) ? n1 : n2;
}
function makeCurvedRimPoints(corners, curvePercents, segments = 18) {
const center = corners.reduce((acc, p) => acc.add(p), vec(0, 0, 0)).multiplyScalar(1 / corners.length);
const rim = [];
const edgeNames = ["top", "right", "bottom", "left"];
for (let i = 0; i < corners.length; i++) {
const a = corners[i];
const b = corners[(i + 1) % corners.length];
const edgeLen = a.distanceTo(b);
const pct = curvePercents[edgeNames[i]] / 100;
const depth = edgeLen * pct;
const n = inwardNormal2D(a, b, center);
const control = a.clone().add(b).multiplyScalar(0.5).add(n.multiplyScalar(depth));
for (let s = 0; s < segments; s++) {
const t = s / segments;
const p = a.clone().multiplyScalar((1 - t) * (1 - t))
.add(control.clone().multiplyScalar(2 * (1 - t) * t))
.add(b.clone().multiplyScalar(t * t));
rim.push(p);
}
}
return rim;
}
function makeSailGeometry(corners, curvePercents) {
const center = corners.reduce((acc, p) => acc.add(p), vec(0, 0, 0)).multiplyScalar(1 / corners.length);
const rim = makeCurvedRimPoints(corners, curvePercents, 18);
const vertices = [center, ...rim];
const positions = new Float32Array(vertices.flatMap((p) => [p.x, p.y, p.z]));
const indices = [];
for (let i = 1; i <= rim.length; i++) {
const next = i === rim.length ? 1 : i + 1;
indices.push(0, i, next);
}
const geo = new THREE.BufferGeometry();
geo.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geo.setIndex(indices);
geo.computeVertexNormals();
return geo;
}
function flattenCornersTo2D(corners) {
const p0 = corners[0];
const p1 = corners[1];
const p2 = corners[2];
const p3 = corners[3];
const xAxis = p1.clone().sub(p0).normalize();
const normal = p1.clone().sub(p0).cross(p3.clone().sub(p0)).normalize();
const yAxis = normal.clone().cross(xAxis).normalize();
return [p0, p1, p2, p3].map((p) => {
const d = p.clone().sub(p0);
return vec(d.dot(xAxis), 0, d.dot(yAxis));
});
}
function downloadTextFile(filename, content, mime = "image/svg+xml") {
const blob = new Blob([content], { type: mime });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
function buildSailSvg(params, corners3D) {
const flatCorners = flattenCornersTo2D(corners3D);
const rim = makeCurvedRimPoints(flatCorners, {
top: params.curveTop,
right: params.curveRight,
bottom: params.curveBottom,
left: params.curveLeft,
}, 36);
const ptsCm = rim.map((p) => ({ x: p.x * 100, y: p.z * 100 }));
const minX = Math.min(...ptsCm.map((p) => p.x));
const maxX = Math.max(...ptsCm.map((p) => p.x));
const minY = Math.min(...ptsCm.map((p) => p.y));
const maxY = Math.max(...ptsCm.map((p) => p.y));
const pad = 25;
const w = maxX - minX + pad * 2;
const h = maxY - minY + pad * 2;
const path = ptsCm.map((p, i) => `${i === 0 ? "M" : "L"} ${fmt(p.x - minX + pad, 2)} ${fmt(p.y - minY + pad, 2)}`).join(" ") + " Z";
const cornerPts = flatCorners.map((p) => ({ x: p.x * 100 - minX + pad, y: p.z * 100 - minY + pad }));
const labels = ["M1", "M2", "A2", "A1"].map((name, i) =>
`${name} `
).join("
");
const cornerMarks = cornerPts.map((p) => ` `).join("
");
return `
`;
}
function Post({ base, top, color }) {
const mid = base.clone().add(top).multiplyScalar(0.5);
const dir = top.clone().sub(base);
const len = dir.length();
const quat = new THREE.Quaternion().setFromUnitVectors(vec(0, 1, 0), dir.clone().normalize());
return (
);
}
function Connector({ from, to }) {
const curve = useMemo(() => new THREE.LineCurve3(from, to), [from, to]);
const tube = useMemo(() => new THREE.TubeGeometry(curve, 1, 0.012, 8), [curve]);
return (
);
}
function SailScene({ params }) {
const group = useRef();
const model = useMemo(() => computeModel(params), [params]);
const sailGeo = useMemo(
() => makeSailGeometry(model.corners, {
top: params.curveTop,
right: params.curveRight,
bottom: params.curveBottom,
left: params.curveLeft,
}),
[model.corners, params.curveTop, params.curveRight, params.curveBottom, params.curveLeft]
);
useFrame((_, delta) => {
if (params.autoRotate && group.current) group.current.rotation.y += delta * 0.18;
});
return (
{Object.entries(model.bases).map(([key, base]) => (
))}
{model.order.map((key) => (
))}
{model.order.map((key) => (
);
}
function NumberField({ label, value, onChange, suffix = "cm" }) {
return (
);
}
export default function Napvitorla3DTervezo() {
const [params, setParams] = useState({
widthCm: 500,
depthCm: 400,
M1: 300,
M2: 300,
A1: 240,
A2: 240,
offsetM1: 40,
offsetM2: 40,
offsetA1: 40,
offsetA2: 40,
curveTop: 6,
curveRight: 6,
curveBottom: 6,
curveLeft: 6,
sailColor: "#d6bd8f",
postColor: "#30343b",
autoRotate: true,
});
const model = useMemo(() => computeModel(params), [params]);
const lengths = useMemo(() => {
const c = model.corners;
return {
top: edgeLength(c[0], c[1]),
right: edgeLength(c[1], c[2]),
bottom: edgeLength(c[2], c[3]),
left: edgeLength(c[3], c[0]),
diag1: edgeLength(c[0], c[2]),
diag2: edgeLength(c[1], c[3]),
};
}, [model]);
const update = (key, value) => setParams((p) => ({ ...p, [key]: value }));
const exportSvg = () => {
const svg = buildSailSvg(params, model.corners);
downloadTextFile("napvitorla-szabasminta.svg", svg);
};
const setHigh1 = (v) => setParams((p) => ({ ...p, M1: v, M2: p.M2 === p.M1 ? v : p.M2 }));
const setLow1 = (v) => setParams((p) => ({ ...p, A1: v, A2: p.A2 === p.A1 ? v : p.A2 }));
const setBaseOffset = (v) => setParams((p) => ({ ...p, offsetM1: v, offsetM2: v, offsetA1: v, offsetA2: v }));
const setBaseCurve = (v) => setParams((p) => ({ ...p, curveTop: v, curveRight: v, curveBottom: v, curveLeft: v }));
return (
update("widthCm", v)} />
update("depthCm", v)} />
update("M2", v)} />
update("A2", v)} />
update("offsetM1", v)} />
update("offsetM2", v)} />
update("offsetA1", v)} />
update("offsetA2", v)} />
update(key, v)} />
))}
update("autoRotate", v)} />
);
}
{key}
))}
onChange(Number(e.target.value))} className="h-9" />
{suffix}
Napvitorla 3D tervező
Négyszög napvitorla, 8° kifelé döntött oszlopokkal, ívelt élekkel.
setBaseOffset(Number(e.target.value))} />
setBaseCurve(Number(e.target.value))} />
{[
["Felső él", "curveTop"],
["Jobb él", "curveRight"],
["Alsó él", "curveBottom"],
["Bal él", "curveLeft"],
].map(([label, key]) => (
{label}{params[key]}%
Gyártási ellenőrző méretek
Felső él:{fmt(lengths.top)} cm
Jobb él:{fmt(lengths.right)} cm
Alsó él:{fmt(lengths.bottom)} cm
Bal él:{fmt(lengths.left)} cm
Átló 1:{fmt(lengths.diag1)} cm
Átló 2:{fmt(lengths.diag2)} cm