bx-2376-new_type #86

Merged
ksenia_mikhailova merged 13 commits from bx-2376-new_type into dev 2025-03-20 10:57:21 +03:00
28 changed files with 432 additions and 177 deletions

View File

@ -6,6 +6,5 @@ import '@/assets/main.scss'
<Header />
<NuxtPage />
<Footer />
<Modal />
</div>
</template>

View File

@ -27,21 +27,18 @@ const cameraStat = reactive({
</div>
</template>
<Loader />
<TresCanvas clear-color="#e2e8f0">
<Scene :canvasProps="{ clearColor: '#e2e8f0' }">
<TresPerspectiveCamera v-bind="cameraStat" ref="camera" />
<OrbitControls v-bind="controlsState" make-default />
<Suspense>
<ModelSmoothCamera />
</Suspense>
<Suspense>
<ModelEnv />
</Suspense>
<TresGroup>
<Suspense>
<ModelParametric />
</Suspense>
</TresGroup>
</TresCanvas>
</Scene>
</ClientOnly>
</div>
</template>

View File

@ -19,6 +19,7 @@ const section_count = use_section_count()
const extra_section = use_extra_section()
const total_length = use_total_length()
const min_length = use_min_length()
const globalFenceType = useGlobalFenceType()
if (!pillar_color.value) {
const r = Math.floor(Math.random() * predefPillarColors.length)
@ -205,7 +206,8 @@ const calc_table = computed(() => {
:goto_cam="new Vector3(1, 2, -1)" />
<label for="pillar_topper">Колпак столба</label>
<DropdownPicker type="topper" :cb="setPillarTopper" name="pillar_topper"
:pattern="getTopper(pillar_topper)" :disabled="remove_pillar"
:pattern="getTopper(pillar_topper, globalFenceType?.type == 'aristo' ? 'aristoToppers' : 'baseToppers')"
:disabled="remove_pillar"
:goto_target="new Vector3(fence_section * -0.5, lamelles_count * lamelle_height, 0)"
:goto_cam="new Vector3(-1, 2, 1)" />
</div>

View File

@ -1,24 +1,39 @@
<script setup lang="ts">
import { getFilename as getPattern, patterns } from '../pattern';
import { getFilename as getTopper, toppers } from '../topper';
import { getFilename as getTopper, baseToppers, aristoToppers } from '../topper';
const globalFenceType = useGlobalFenceType()
const props = defineProps(['cb', 'patterns']);
type pType = {
list: typeof patterns | typeof toppers,
list: typeof patterns | typeof baseToppers,
func: any
type: string | null
}
const p = {
const p = reactive({
list: [],
type: null,
func: () => { }
} as pType
} as pType)
if (props.patterns == 'pattern') {
p.list = patterns;
p.func = getPattern
} else if (props.patterns == 'topper') {
p.list = toppers
p.list = baseToppers
p.func = getTopper
}
watch(globalFenceType, () => {
if (props.patterns == 'topper') {
if (globalFenceType.value?.type == 'standart') {
p.list = baseToppers
p.type = 'toppers'
}
if (globalFenceType.value?.type == 'aristo') {
p.list = aristoToppers
p.type = 'aristoToppers'
}
}
})
</script>
<template>
<template v-for="item in p.list">

View File

@ -17,22 +17,6 @@ const explosion_state = use_explosion_state()
const toggleExpState = () => {
explosion_state.value = !explosion_state.value
}
const back_light = ref()
const secondary_light = ref()
const loadAll = async () => {
const { scene: back } = await useGLTF('/models_light/back_exp.glb')
const { scene: secondary } = await useGLTF('/models_light/secondary_exp.glb')
const k = 0.03
back_light.value = back.children[0]
back_light.value.intensity = back_light.value.intensity * k
back_light.value.shadow.bias = -0.02
secondary_light.value = secondary.children[0]
secondary_light.value.intensity = secondary_light.value.intensity * k
secondary_light.value.shadow.bias = -0.01
}
const changeDistance = (v = 1) => {
if (camera.value && controls.value) {
@ -41,9 +25,6 @@ const changeDistance = (v = 1) => {
camera.value.position.normalize().multiplyScalar(r)
}
}
onMounted(() => {
loadAll()
})
</script>
<template>
<div class="h-96 relative">
@ -53,14 +34,11 @@ onMounted(() => {
Загрузка 3D модели
</div>
</template>
<TresCanvas height="600">
<Scene :canvasProps="{ height: '600' }">
<TresPerspectiveCamera :position="[-7, 2, 4]" ref="camera" />
<OrbitControls v-bind="controlsState" ref="controls" make-default />
<ModelEnv />
<Suspense>
<ModelDiagram />
</Suspense>
</TresCanvas>
<ModelDiagram />
</Scene>
</ClientOnly>
<div class="canvas-icons">
<a href="#" @click.prevent="toggleExpState">

View File

@ -7,7 +7,7 @@ import { getName as getTopperName } from './topper';
const config = useRuntimeConfig()
const apiBase = config.public.apiBase
const { data: calculatorData } = await apiFetch<ApiCalcType>(`calculator/5/`)
const props = defineProps(['calcData'])
const isModalOpen = useState('modal_open', () => false)
const lamelle_height = useState<number>('lamelle_height')
@ -122,12 +122,14 @@ const total_colors = computed(() => {
`Столбы ${pillar_color.value} ${getColorNameFromRal(pillar_color.value)}`,
]
})
const total_txt = computed(() => {
if (!calculatorData.value) return
const pillar = parseFloat(calculatorData.value.pillar)
const pillar_base = parseFloat(calculatorData.value.pillar_base)
const lamelles_block = parseFloat(calculatorData.value.lamelles_block)
const { discount } = calculatorData.value
const calculatorData = props.calcData as ApiCalcType
if (!calculatorData) return
const pillar = parseFloat(calculatorData.pillar)
const pillar_base = parseFloat(calculatorData.pillar_base)
const lamelles_block = parseFloat(calculatorData.lamelles_block)
const { discount } = calculatorData
const sections = section_count.value as number
const extra_m = extra_section.value as number * 0.001

View File

@ -4,6 +4,7 @@ import { useGLTF } from '@tresjs/cientos'
import { getColorHexFromRal } from '../ral';
const explosion_state = use_explosion_state()
const globalFenceType = useGlobalFenceType()
const k = 1.5
const targetExplosion = {
@ -14,24 +15,67 @@ const targetExplosion = {
stolb: [0 * k, 0 * k, 1 * k],
verh: [0 * k, 0.25 * k, 0 * k],
}
const { scene: standart_kosynka } = await useGLTF('/models_exp/kosynka.glb')
const { scene: standart_krepleniye_planok } = await useGLTF('/models_exp/krepleniye_planok.glb')
const { scene: standart_osnova_stolba } = await useGLTF('/models_exp/osnova_stolba.glb')
const { scene: standart_planki } = await useGLTF('/models_exp/planki.glb')
const { scene: standart_stolb } = await useGLTF('/models_exp/stolb.glb')
const { scene: standart_verh } = await useGLTF('/models_exp/verh.glb')
const { scene: aristo_kosynka } = await useGLTF('/models_aristo_exp/kosynka.glb')
const { scene: aristo_krepleniye_planok } = await useGLTF('/models_aristo_exp/krepleniye_planok.glb')
const { scene: aristo_osnova_stolba } = await useGLTF('/models_aristo_exp/osnova_stolba.glb')
const { scene: aristo_planki } = await useGLTF('/models_aristo_exp/planki.glb')
const { scene: aristo_stolb } = await useGLTF('/models_aristo_exp/stolb.glb')
const { scene: aristo_verh } = await useGLTF('/models_aristo_exp/verh.glb')
const kosynka = ref()
const krepleniye_planok = ref()
const osnova_stolba = ref()
const planki = ref()
const stolb = ref()
const verh = ref()
const setModels = () => {
const t = globalFenceType.value?.type || 'aristo'
if (t == 'aristo') {
kosynka.value = aristo_kosynka
krepleniye_planok.value = aristo_krepleniye_planok
osnova_stolba.value = aristo_osnova_stolba
planki.value = aristo_planki
stolb.value = aristo_stolb
verh.value = aristo_verh
}
if (t == "standart") {
kosynka.value = standart_kosynka
krepleniye_planok.value = standart_krepleniye_planok
osnova_stolba.value = standart_osnova_stolba
planki.value = standart_planki
stolb.value = standart_stolb
verh.value = standart_verh
}
}
setModels()
watch(() => globalFenceType.value?.type, (t) => {
setModels()
set_material(planki.value, getColorHexFromRal(lamelle_color.value));
[stolb, verh, krepleniye_planok].map(el => set_material(el.value, getColorHexFromRal(pillar_color.value)))
})
const { scene: kosynka } = await useGLTF('/models_exp/kosynka.glb')
const { scene: krepleniye_planok } = await useGLTF('/models_exp/krepleniye_planok.glb')
const { scene: osnova_stolba } = await useGLTF('/models_exp/osnova_stolba.glb')
const { scene: planki } = await useGLTF('/models_exp/planki.glb')
const { scene: stolb } = await useGLTF('/models_exp/stolb.glb')
const { scene: verh } = await useGLTF('/models_exp/verh.glb')
const lamelle_color = use_lamelle_color()
const pillar_color = use_pillar_color()
set_material(planki, getColorHexFromRal(lamelle_color.value));
[stolb, verh, krepleniye_planok].map(el => set_material(el, getColorHexFromRal(pillar_color.value)))
set_material(planki.value, getColorHexFromRal(lamelle_color.value));
[stolb, verh, krepleniye_planok].map(el => set_material(el.value, getColorHexFromRal(pillar_color.value)))
</script>
<template>
<Suspense>
<TresGroup :position-y="-3.5" :scale="2">
<TresGroup :position-y="!globalFenceType || globalFenceType.type == 'aristo' ? -2 : -3.5" :scale="2"
:key="globalFenceType?.type">
<ModelItem :model="kosynka" :target="explosion_state ? targetExplosion.kosynka : [0, 0, 0]" />
<ModelItem :model="krepleniye_planok"
:target="explosion_state ? targetExplosion.krepleniye_planok : [0, 0, 0]" />

View File

@ -32,7 +32,7 @@ onMounted(async () => {
const newEnvMap = exrCubeRenderTarget ? exrCubeRenderTarget.texture : null;
scene.value.environment = newEnvMap
scene.value.environmentIntensity = 1
scene.value.environmentIntensity = 1.25
scene.value.environmentRotation.z = 0.25
result.renderTarget.texture.dispose();
})

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { BufferGeometry, Matrix4, Mesh, Object3D, Vector3 } from 'three';
import { BufferGeometry, Group, Matrix4, Mesh, Object3D, Vector3 } from 'three';
import { getColorHexFromRal } from '../ral';
const props = defineProps(['index', 'models', 'last_element', 'first_element'])
@ -16,7 +16,6 @@ const pillar_pattern = use_pattern()
const pillar_topper = use_topper()
const lamelle_color = use_lamelle_color()
const lSize = lamelle_height.value
const pillar_size = 104 * 0.001
const pillar_one_pos = ref()
@ -80,7 +79,7 @@ const instanced_fixing_two_el = [
const lamelleMatrix = (i: number) => {
const scale_x = (((extra.value as number) || fence_section.value) * 9.9)
const pos_x = pillar_size * 0.5
const pos_y = (lSize * i)
const pos_y = (lamelle_height.value * i)
const pos_z = 0.022 * scale_koef
return new Matrix4().fromArray([
scale_x, 0, 0, 0,
@ -91,7 +90,7 @@ const lamelleMatrix = (i: number) => {
}
const fixingOneMatrix = (i: number) => {
const pos_x = pillar_one_pos.value + pillar_size * 0.66
const pos_y = (lSize * i) + 0.01 * scale_koef;
const pos_y = (lamelle_height.value * i) + 0.01 * scale_koef;
const pos_z = 0.022 * scale_koef
return new Matrix4().fromArray([
1, 0, 0, 0,
@ -102,7 +101,7 @@ const fixingOneMatrix = (i: number) => {
}
const fixingTwoMatrix = (i: number) => {
const pos_x = pillar_two_pos.value - pillar_size * 0.66
const pos_y = (lSize * i) + 0.01 * scale_koef;
const pos_y = (lamelle_height.value * i) + 0.01 * scale_koef;
const pos_z = 0.022 * scale_koef
return new Matrix4().fromArray([
1, 0, 0, 0,
@ -130,19 +129,35 @@ const setBraceCount = () => {
setBraceCount()
watch(lamelles_count, setBraceCount)
const calculateAddScale = (model: Mesh, k = 1): number => {
if (!model.geometry.boundingBox) {
model.geometry.computeBoundingBox(); // Вычисляем boundingBox, если он не был вычислен ранее
}
const boundingBoxHeight = Math.abs(
model.geometry.boundingBox!.max.y - model.geometry.boundingBox!.min.y
);
if (boundingBoxHeight === 0) {
console.warn('Bounding box height is zero. Returning default scale of 1.');
return 1; // Возвращаем 1 при делении на ноль
}
return (lamelle_height.value * k) / boundingBoxHeight;
};
const pillar = ref<Mesh[]>([])
const setPillar = () => {
const top = props.models.pillar_top.children[0];
top.position.setComponent(1, lSize * lamelles_count.value + lamelles_count.value * 0.0001 * scale_koef);
top.position.setComponent(1, lamelle_height.value * lamelles_count.value * scale_koef);
const pillar_outer = props.models.pillar_center.children[0];
pillar_outer.scale.setComponent(1, lamelles_count.value);
pillar_outer.scale.setComponent(1, lamelles_count.value * calculateAddScale(pillar_outer));
const pillar_inner = props.models.pillar_inner.children[0];
pillar_inner.scale.setComponent(1, lamelles_count.value);
pillar_inner.scale.setComponent(1, lamelles_count.value * calculateAddScale(pillar_inner));
const bottom = props.models.pillar_bottom.children[0];
bottom.position.setComponent(1, lSize * -0.5);
let arr = [top, pillar_outer, pillar_inner, bottom]
arr.map(el => {
@ -151,48 +166,50 @@ const setPillar = () => {
set_material(
{ children: [arr[2]] },
getColorHexFromRal(pillar_color.value),
{ pattern: pillar_pattern.value, count: lamelles_count.value },
{ pattern: pillar_pattern.value, count: lamelles_count.value * calculateAddScale(pillar_inner) },
true
)
pillar.value = arr.map(el => el.clone())
}
setPillar()
watch([pillar_pattern, pillar_color, pillar_topper, fence_section, lamelles_count], setPillar)
watch([pillar_pattern, pillar_color, pillar_topper, fence_section, lamelles_count, lamelle_height], setPillar)
const fastening = ref<Object3D[]>([])
const setFastening = () => {
const top_one = props.models.fixing.clone().children[0];
top_one.position.set(
pillar_one_pos.value + pillar_size * 0.66,
lamelles_count.value * lSize - 0.015 * scale_koef,
lamelles_count.value * lamelle_height.value - 0.015 * scale_koef,
0.025 * scale_koef
)
const top_two = props.models.fixing.clone().children[0];
top_two.position.set(
pillar_two_pos.value - pillar_size * 0.66,
lamelles_count.value * lSize - 0.01 * scale_koef,
lamelles_count.value * lamelle_height.value - 0.01 * scale_koef,
0.025 * scale_koef
)
const v = ((extra.value as number) || fence_section.value) * 10
const top = props.models.fastening_top.clone().children[0];
top.position.set(
pillar_size * 0.5,
lamelles_count.value * lSize - 0.0275 * scale_koef,
lamelles_count.value * lamelle_height.value -0.0275 * scale_koef,
0
);
top.scale.setComponent(0, v);
let c = 0.0019 * scale_koef
const side_one = props.models.fastening_side.clone().children[0];
side_one.name = 'side_one'
side_one.position.set(pillar_one_pos.value, 0, 0.002 * scale_koef);
side_one.scale.set(1, lamelles_count.value, 1)
side_one.position.set(pillar_one_pos.value, 0, c);
side_one.scale.set(1, lamelles_count.value * calculateAddScale(side_one), 1)
const side_two = props.models.fastening_side.clone().children[0];
side_two.name = 'side_two'
side_two.scale.set(-1, lamelles_count.value, -1)
side_two.position.set(pillar_two_pos.value, 0, -0.005 * scale_koef);
side_two.scale.set(-1, lamelles_count.value * calculateAddScale(side_two), -1)
side_two.position.set(pillar_two_pos.value, 0, c);
let arr = [top_one, top_two, top, side_one, side_two];
[top, side_one, side_two, ...braces.value].map(el => {
@ -201,7 +218,7 @@ const setFastening = () => {
fastening.value = arr.map(el => el.clone())
}
setFastening()
watch([pillar_color, lamelles_count, pillar_one_pos, pillar_two_pos], setFastening)
watch([pillar_color, pillar_one_pos, pillar_two_pos, lamelles_count, lamelle_height], setFastening)
const setLamellesColor = () => {
if (instanced_lamelle.value) {
@ -214,6 +231,7 @@ watch([instanced_lamelle, lamelle_color], setLamellesColor)
watch([
instanced_lamelle,
lamelles_count,
lamelle_height,
fence_section,
], () => {
const translationVector = new Vector3(0, 20, 20)

View File

@ -3,7 +3,7 @@ import { Object3D, Vector3 } from 'three';
//@ts-ignore
import { useGLTF, } from '@tresjs/cientos'
import type { OrbitControlsProps } from '@tresjs/cientos/dist/core/controls/OrbitControls.vue.js';
import { getModel, toppers, type toppersIds } from '../topper';
import { getModel, allToppers, baseToppers, aristoToppers, type toppersIds } from '../topper';
const section_count = use_section_count()
const extra_section = use_extra_section()
@ -20,103 +20,156 @@ const goto_target = use_goto_target()
const { scene, controls, camera } = useTresContext()
const { seek, seekAll } = useSeek()
const topper_models = {} as { [key: toppersIds]: Object3D }
for await (const element of toppers) {
const { scene } = await useGLTF(getModel(element.id))
topper_models[element.id] = scene
}
const globalFenceType = useGlobalFenceType()
const { scene: model_pillar_center } = await useGLTF('/models_one/pillar/center.glb')
const { scene: model_pillar_bottom } = await useGLTF('/models_one/pillar/bottom.glb')
const { scene: model_pillar_inner } = await useGLTF('/models_one/pillar/inner.glb')
const { scene: model_pillar_brace } = await useGLTF('/models_one/pillar/brace.glb')
const top = ref(null)
const pillar_top = ref()
const pillar_center = ref(null)
const pillar_bottom = ref(null)
const pillar_inner = ref(null)
const pillar_brace = ref(null)
const fastening_top = ref(null)
const fastening_side = ref(null)
const fixing = ref(null)
const lamelle = ref(null)
const { scene: model_fastening_top } = await useGLTF('/models_one/fastening/top.glb');
const { scene: model_fastening_side } = await useGLTF('/models_one/fastening/side.glb');
const { scene: model_fixing } = await useGLTF('/models_one/fixing.glb');
const total = ref(0)
const size = ref(0)
const count = ref(0)
const { scene: top_model } = await useGLTF('/models_one/top_100.glb', { draco: true })
const { scene: lamelle_model } = await useGLTF('/models_one/lamel_100.glb', { draco: true });
const top = ref(top_model)
const pillar_top = ref(topper_models[pillar_topper.value])
const pillar_center = ref(model_pillar_center)
const pillar_bottom = ref(model_pillar_bottom)
const pillar_inner = ref(model_pillar_inner)
const pillar_brace = ref(model_pillar_brace)
const fastening_top = ref(model_fastening_top)
const fastening_side = ref(model_fastening_side)
const fixing = ref(model_fixing)
const lamelle = ref(lamelle_model)
watch(pillar_topper, async () => {
pillar_top.value = topper_models[pillar_topper.value]
})
const total = ref((section_count.value + ~~(!!extra_section.value)))
const size = ref(Math.ceil(total.value / 4))
const count = ref((total.value >= 4) ? size.value : total.value)
watch(() => [section_count.value, extra_section.value], () => {
total.value = (section_count.value + ~~(!!extra_section.value))
size.value = Math.ceil(total.value / 4);
count.value = (total.value >= 4) ? size.value : total.value;
const lines_count = (total.value >= 4) ? 4 : 1
const base = seek(scene.value, 'name', 'base')
if (base?.children && base.children.length !== lines_count) {
base.children = [...base?.children.slice(0, lines_count)]
}
const lines = seekAll(scene.value, 'name', 'line')
lines.forEach(line => {
let n = size.value
if (lines_count == 1) {
n = total.value
}
if (line.name.endsWith('_4')) {
n = total.value - size.value * 3
if (n < 0) {
n = 0
}
}
const inner = seek(line, 'name', line.name + '_inner');
if (inner?.children && n < inner?.children.length) {
inner.children = [...inner?.children.slice(0, n)]
}
});
})
const setTarget = (smooth = false) => {
let f = fence_section.value * lamelles_count.value * lamelle_height.value * 0.75;
const max = 2.25
if (f < max) f = max
const target = new Vector3(0, lamelles_count.value * lamelle_height.value * 0.5, 0);
if (smooth) {
goto_target.value = target
goto_cam.value = new Vector3(f, f, f)
} else {
(controls.value as OrbitControlsProps).target = target;
(controls.value as any).update()
goto_cam.value = new Vector3(f, f, f)
}
}
watch([lamelles_count, fence_section], () => {
setTarget()
open_calc.value = []
})
watch(open_calc, () => {
if (Object.keys(open_calc.value).length == 0) {
setTarget(true)
}
})
const min_for_square = 12;
setTarget()
const loadAll = async () => {
const topper_models = {
baseToppers: {} as { [key: toppersIds]: Object3D },
aristoToppers: {} as { [key: toppersIds]: Object3D },
}
for (const key of Object.keys(allToppers)) {
let toppers = allToppers[key]
for await (const element of toppers) {
const { scene } = await useGLTF(getModel(element.id, key))
topper_models[key][element.id] = scene
}
}
const { scene: model_pillar_center } = await useGLTF('/models_one/pillar/center.glb')
const { scene: model_pillar_bottom } = await useGLTF('/models_one/pillar/bottom.glb')
const { scene: model_pillar_inner } = await useGLTF('/models_one/pillar/inner.glb')
const { scene: model_pillar_brace } = await useGLTF('/models_one/pillar/brace.glb')
const { scene: model_fastening_top } = await useGLTF('/models_one/fastening/top.glb');
const { scene: model_fastening_side } = await useGLTF('/models_one/fastening/side.glb');
const { scene: model_fixing } = await useGLTF('/models_one/fixing.glb');
const { scene: top_model } = await useGLTF('/models_one/top_100.glb', { draco: true })
// const { scene: lamelle_model } = await useGLTF('/models_one/lamel_100.glb', { draco: true });
const { scene: lamelle_model_standart } = await useGLTF('/models_one/lamel_100.glb', { draco: true });
const { scene: lamelle_model_aristo } = await useGLTF('/models_one/lamel_100_aristo.glb', { draco: true });
top.value = top_model
pillar_center.value = model_pillar_center
pillar_bottom.value = model_pillar_bottom
pillar_inner.value = model_pillar_inner
pillar_brace.value = model_pillar_brace
fastening_top.value = model_fastening_top
fastening_side.value = model_fastening_side
fixing.value = model_fixing
lamelle.value = lamelle_model_standart
const setPillarTopper = () => {
let key = 'baseToppers'
if (globalFenceType.value?.type == 'aristo') {
key = 'aristoToppers'
}
pillar_top.value = topper_models[key][pillar_topper.value]
}
setPillarTopper()
watch(pillar_topper, setPillarTopper)
total.value = (section_count.value + ~~(!!extra_section.value))
size.value = Math.ceil(total.value / 4)
count.value = (total.value >= 4) ? size.value : total.value
watch(() => [section_count.value, extra_section.value], () => {
total.value = (section_count.value + ~~(!!extra_section.value))
size.value = Math.ceil(total.value / 4);
count.value = (total.value >= 4) ? size.value : total.value;
const lines_count = (total.value >= 4) ? 4 : 1
const base = seek(scene.value, 'name', 'base')
if (base?.children && base.children.length !== lines_count) {
base.children = [...base?.children.slice(0, lines_count)]
}
const lines = seekAll(scene.value, 'name', 'line')
lines.forEach(line => {
let n = size.value
if (lines_count == 1) {
n = total.value
}
if (line.name.endsWith('_4')) {
n = total.value - size.value * 3
if (n < 0) {
n = 0
}
}
const inner = seek(line, 'name', line.name + '_inner');
if (inner?.children && n < inner?.children.length) {
inner.children = [...inner?.children.slice(0, n)]
}
});
})
const setTarget = (smooth = false) => {
let f = fence_section.value * lamelles_count.value * lamelle_height.value * 0.75;
const max = 2.25
if (f < max) f = max
const target = new Vector3(0, lamelles_count.value * lamelle_height.value * 0.5, 0);
if (smooth) {
goto_target.value = target
goto_cam.value = new Vector3(f, f, f)
} else {
(controls.value as OrbitControlsProps).target = target;
(controls.value as any).update()
goto_cam.value = new Vector3(f, f, f)
}
}
watch([lamelles_count, fence_section, lamelle_height], () => {
setTarget()
open_calc.value = []
})
watch(open_calc, () => {
if (Object.keys(open_calc.value).length == 0) {
setTarget(true)
}
})
setTarget()
const setLamelleType = () => {
if (globalFenceType.value?.type == 'standart') {
lamelle.value = lamelle_model_standart
lamelle_height.value = 0.115
pillar_topper.value = 0
setPillarTopper()
}
if (globalFenceType.value?.type == 'aristo') {
lamelle.value = lamelle_model_aristo
lamelle_height.value = 0.196
pillar_topper.value = 0
setPillarTopper()
}
}
setLamelleType()
watch(() => globalFenceType.value?.id, setLamelleType)
}
onMounted(async () => { await loadAll() })
</script>
<template>
<TresGroup name="base">
<template v-for="line in (total >= min_for_square) ? 4 : 1" :key="`${line}_${count}`">
<template v-for="line in (total >= min_for_square) ? 4 : 1"
:key="`${globalFenceType?.id ?? 5}_${line}_${count}`">
<ModelLine :models="{
top,
pillar_center, pillar_top, pillar_bottom, pillar_inner,

55
components/scene.vue Normal file
View File

@ -0,0 +1,55 @@
<script setup>
const props = defineProps({
canvasProps: {
type: Object,
default: () => ({}),
},
cameraProps: {
type: Object,
default: () => ({}),
}
});
const container = ref(null);
const isIntersecting = ref(true);
let observer;
// Вычисляем renderMode на основе видимости
const renderMode = computed(() => (isIntersecting.value ? 'always' : 'never'));
const startObserver = () => {
observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
isIntersecting.value = entry.isIntersecting;
});
},
{ threshold: 0.1 } // Настройте порог видимости по вашему усмотрению
);
if (container.value) {
observer.observe(container.value);
}
}
onMounted(async () => {
await nextTick(); // Ждём завершения рендеринга
startObserver(); // Запускаем IntersectionObserver
});
onBeforeUnmount(() => {
if (observer) {
observer.disconnect();
}
});
</script>
<template>
<div ref="container" class="h-full">
<Suspense>
<TresCanvas v-bind="canvasProps" :render-mode="renderMode" :key="renderMode">
<ModelEnv />
<Suspense>
<slot />
</Suspense>
</TresCanvas>
</Suspense>
</div>
</template>

View File

@ -1,24 +1,63 @@
export const toppers = [
interface Topper {
id: number; // ID элемента
name: string; // Название
filename: string; // Имя файла
model: string; // Модель
}
export const baseToppers = [
{ name: 'Ровный', filename: 'icon_stolb_verh_3.svg', model: 'top' },
{ name: 'Вершина 1', filename: 'icon_stolb_verh_2.svg', model: 'decor1' },
{ name: 'Вершина 2', filename: 'icon_stolb_verh_1.svg', model: 'decor2' },
].map((el, i) => Object.assign(el, { id: i }))
export const getFilename = (id: toppersIds) => {
const el = toppers.find(el => el.id == id)
export const aristoToppers = [
{ name: 'Колпак', filename: 'icon_stolb_verh_1.svg', model: 'aristo1' },
].map((el, i) => Object.assign(el, { id: i }))
export const allToppers = {
'baseToppers': baseToppers,
'aristoToppers': aristoToppers,
} as { [key: string]: Topper[] }
function getToppersByType(type: string | null): any {
if (!type) type = 'NONE'
// Проверяем, существует ли указанный тип в объекте allToppers
if (allToppers[type]) {
return allToppers[type];
}
// Если типа нет, берем первый доступный элемент из allToppers
const keys = Object.keys(allToppers);
if (keys.length > 0) {
const firstKey = keys[0];
return allToppers[firstKey];
}
// Если объект allToppers пустой, возвращаем значение по умолчанию (например, пустой массив)
return [];
}
export const getFilename = (id: toppersIds, type: string | null) => {
let toppers = getToppersByType(type)
const el = toppers.find((el: Topper) => el.id == id)
if (!el || !el.filename) return undefined
return `/topper/${el?.filename}`
}
export const getName = (id: toppersIds) => {
const el = toppers.find(el => el.id == id)
export const getName = (id: toppersIds, type: string | null) => {
let toppers = getToppersByType(type)
const el = toppers.find((el: Topper) => el.id == id)
if (!el || !el.filename) return undefined
return el.name
}
export const getModel = (id: toppersIds) => {
const el = toppers.find(el => el.id == id)
export const getModel = (id: toppersIds, type: string | null) => {
let toppers = getToppersByType(type)
let el = toppers.find((el: Topper) => el.id == id)
if (!el || !el.filename) return undefined
return `/models_one/pillar/topper/${el?.model}.glb`
}
export type toppersIds = typeof toppers[number]['id']
export type toppersIds = typeof allToppers[string][number]['id']

View File

@ -0,0 +1,7 @@
interface calc {
id: number
type: 'standart' | 'aristo'
calc: ApiCalcType
}
export const useGlobalFenceType = () => useState<calc | null>('fence-global-type', () => null)

View File

@ -32,7 +32,7 @@ if(route.path !== '/404') {
<h1>Вы ищете страницу, которой не существует. Вернитесь на главную страницу сайта</h1>
<p>Извините, но мы не можем найти запрашиваемую страницу. К сожалению, мы не можем помочь вам с
покупкой забора здесь.</p>
<p class="hidden">
<p>
<code>
{{ error?.message }}
</code>

View File

@ -10,6 +10,9 @@ export default defineNuxtConfig({
htmlAttrs: {
lang: 'ru',
},
link: [
{ rel: 'icon', type: 'image/svg+xml', href: '/tabler--fence-filled.svg' },
]
},
},
ssr: true,
@ -25,7 +28,9 @@ export default defineNuxtConfig({
],
runtimeConfig: {
public: {
apiBase: 'https://mns.kustarshina.ru/kp',
// apiBase: process.env.mode == 'DEVELOPMENT' ? "http://localhost:8000" : "https://mns.kustarshina.ru/kp",
apiBase: process.env.mode == 'DEVELOPMENT' ? "http://mns.dev.kustarshina.ru" : "https://mns.kustarshina.ru/kp",
// apiBase: 'http://localhost:8000',
imgBase: 'https://mns.kustarshina.ru',
baseUrl: '',
yandexMetrika: {

View File

@ -7,6 +7,8 @@ import { marked } from 'marked';
import og_img from '/og_img.png'
const globalFenceType = useGlobalFenceType()
const { data: seoData } = await apiFetch<ApiKpType>(`kp/1/`)
useSeoMeta({
title: seoData.value?.title,
@ -21,7 +23,10 @@ const { data: menuData } = await apiFetch<ApiMenuType>(`menu/1/?ordering=order`)
const pagesData = menuData.value ? menuData.value.pages : []
const { data: reviewsData } = await apiFetch<ApiReviewsType[]>(`review/`)
const { data: calculatorData } = await apiFetch(`calculator/5/`)
const { data: calculators } = await apiFetch<ApiCalcType[]>('calculator/')
const openTab = ref<number>()
if (calculators.value?.length) openTab.value = calculators.value[0].id
const about = pagesData.find(el => el.slug == 'about')
const reviews = pagesData.find(el => el.slug == 'clients')
@ -37,16 +42,32 @@ const aboutText = computed(() => marked.parse(about?.content || ''))
const deliveryText = computed(() => delivery?.content.split('[col]').map(el => marked.parse(el || '')))
const advantagesText = computed(() => {
let c = advantages?.content || ''
Object.entries(calculatorData.value || {}).map(item => {
c = c.replaceAll(`[${item[0]}]`, roubleSign.format(item[1]))
Object.entries(calculators.value?.find(el => el.id == openTab.value) || {}).map(item => {
c = c.replaceAll(`[${item[0]}]`, roubleSign.format(item[1] as number))
})
return marked.parse(c)
})
const { data: advData } = await apiFetch<ApiAdvantageType[]>(`advantage/`)
watch(openTab, () => {
if (!openTab.value) return
if (!calculators.value) return
let type = 'standart'
if (openTab.value == 6) {
type = 'aristo'
}
const v = {
id: openTab.value as number,
type: type as "standart" | "aristo",
calc: calculators.value.find(el => el.id == openTab.value) as ApiCalcType,
}
globalFenceType.value = v
})
</script>
<template>
<div>
<Modal :calcData="(calculators || []).find(el => el.id == openTab)" />
<div class="siteblock bg-white" :id="about?.slug" v-if="about">
<div class="container">
<h1 class="siteblock-title">{{ about?.title }}</h1>
@ -92,6 +113,24 @@ const { data: advData } = await apiFetch<ApiAdvantageType[]>(`advantage/`)
</div>
</div>
<div class="siteblock bg-white" :id="advantages?.slug">
<div class="flex col-span-full gap-2 justify-center">
<template v-for="item in calculators">
<div class="flex items-center gap-2">
<label class="rounded p-2 cursor-pointer flex gap-2" :for="item.id.toString()">
<input type="radio" :id="item.id.toString()" :value="item.id" v-model="openTab"
:checked="openTab == item.id">
<span :class="[
'text-2xl font-bold bg-clip-text text-transparent drop-shadow-xs',
openTab == item.id
? 'bg-gradient-to-r from-yellow-400 via-orange-500 to-red-600'
: 'bg-gradient-to-r from-slate-400 to-slate-600'
]">
{{ item.title }}
</span>
</label>
</div>
</template>
</div>
<div class="container gap-4">
<div class="col-span-full xl:col-span-8">
<ExpDiagram />
@ -101,7 +140,7 @@ const { data: advData } = await apiFetch<ApiAdvantageType[]>(`advantage/`)
</div>
</div>
</div>
<div class="siteblock siteblock_imgbg bg-slate-500"
<div class="siteblock siteblock_imgbg bg-slate-500 "
:style="[{ backgroundImage: `url(${[imgBase, delivery?.image].join('/')})` }]">
<NuxtImg :src="[imgBase, delivery?.image].join('/')" class="invisible" v-if="delivery"
alt="коричневый забор" title="" format="webp" loading="lazy" />

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/models_exp.zip Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M19 17v3a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1v-3zm-8 0v3a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1v-3zm9-5a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zM8.707 3.293l2 2A1 1 0 0 1 11 6v5H5V6a1 1 0 0 1 .293-.707l2-2a1 1 0 0 1 1.414 0m8 0l2 2A1 1 0 0 1 19 6v5h-6V6a1 1 0 0 1 .293-.707l2-2a1 1 0 0 1 1.414 0"/></svg>

After

Width:  |  Height:  |  Size: 424 B

View File

@ -3,6 +3,7 @@ export async function apiFetch<T>(path: string) {
const apiBase = config.public.apiBase
const headers = new Headers()
headers.set('Referer', config.public.baseUrl)
return useFetch<T>(`${apiBase}/${path}`, {
baseURL: config.public.baseUrl,
headers,