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 ` Napvitorla szabásminta Egység: cm. Ívelt élek a megadott százalékokkal. ${cornerMarks} ${labels} Napvitorla szabásminta • méretarányos SVG • cm `; } 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) => (
{key}
))}
); } function NumberField({ label, value, onChange, suffix = "cm" }) { return (
onChange(Number(e.target.value))} className="h-9" /> {suffix}
); } 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 (

Napvitorla 3D tervező

Négyszög napvitorla, 8° kifelé döntött oszlopokkal, ívelt élekkel.

update("widthCm", v)} /> update("depthCm", v)} />
update("M2", v)} /> update("A2", v)} />
setBaseOffset(Number(e.target.value))} />
update("offsetM1", v)} /> update("offsetM2", v)} /> update("offsetA1", v)} /> update("offsetA2", v)} />
setBaseCurve(Number(e.target.value))} />
{[ ["Felső él", "curveTop"], ["Jobb él", "curveRight"], ["Alsó él", "curveBottom"], ["Bal él", "curveLeft"], ].map(([label, key]) => (
{label}{params[key]}%
update(key, v)} />
))}
update("autoRotate", v)} />
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
); }