This commit is contained in:
Kseninia Mikhaylova 2024-08-20 15:10:12 +03:00
parent ae540ed6f8
commit 1b635741b9
16 changed files with 221 additions and 494 deletions

View File

@ -13,6 +13,7 @@ declare module 'vue' {
Gallery: typeof import('./src/components/Promo/gallery.vue')['default']
Game: typeof import('./src/components/Game.vue')['default']
Home: typeof import('./src/components/Home.vue')['default']
IMdiArrowRight: typeof import('~icons/mdi/arrow-right')['default']
IMdiChevronLeft: typeof import('~icons/mdi/chevron-left')['default']
IMdiClose: typeof import('~icons/mdi/close')['default']
IMdiFile: typeof import('~icons/mdi/file')['default']

View File

@ -1,36 +0,0 @@
@font-face {
font-family: 'open-sans';
src: url('../fonts/open-sans.ttf');
}
$accentColor: rgb(126, 126, 223);
.container {
font-family: 'open-sans';
}
.header {
display: flex;
justify-content: center;
font-size: 6rem;
font-weight: 800;
}
.main {
ul {
font-size: 4rem;
li {
padding: 2rem;
}
a {
text-decoration: none;
color: initial;
&:hover,
&:active {
color: $accentColor;
}
}
}
}

View File

@ -1,5 +1,16 @@
@import 'grid.scss';
@font-face {
font-family: 'main';
src: url('../fonts/main.otf');
}
@font-face {
font-family: 'logo';
src: url('../fonts/logo.ttf');
}
a[href] {
color: #048280;
@ -7,3 +18,5 @@ a[href] {
color: #a47f62;
}
}
@import 'sidebar.scss';

View File

@ -1,110 +0,0 @@
$blackColor: #181818;
$accentColor: #ef570c;
$redColor: #f83300;
@font-face {
font-family: 'logo';
src: url('../fonts/logo.ttf');
}
@font-face {
font-family: 'main';
src: url('../fonts/main.otf');
}
.container {
font-family: 'main';
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.15) 0%, rgba(0, 0, 0, 0.15) 100%), radial-gradient(at top center, rgba(255, 255, 255, 0.40) 0%, rgba(0, 0, 0, 0.40) 120%) $blackColor;
background-blend-mode: multiply, multiply;
color: #fff;
user-select: none;
}
.header {
justify-content: center;
}
.main,
.header,
.sidebar {
display: flex;
align-items: center;
}
.menu {
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
font-size: 2.5rem;
li {
padding: 0.5rem 0;
a {
text-decoration: none;
&,
&:hover,
&:active,
&:visited {
color: $accentColor;
}
}
}
}
.model {
max-width: 33vw;
}
.product {
gap: 1rem;
position: relative;
&-image {
flex-basis: 50%;
flex-shrink: 0;
}
&-description {
font-size: 1.75rem;
line-height: 1.4;
}
&-model {
max-height: 80%;
&-icon {
position: absolute;
font-size: 4rem;
right: 0;
top: 0;
color: $accentColor;
z-index: 2;
}
}
}
.logo {
&-header {
font-family: 'logo';
font-size: 4rem;
padding: 1rem;
background: linear-gradient(45deg, $redColor, $accentColor);
background-clip: text;
-webkit-text-fill-color: transparent;
}
&-img {
width: 54%;
display: block;
margin: auto;
}
}
img {
max-width: 100%;
}

View File

@ -0,0 +1,115 @@
$bg: #2D3031;
$textColor: #fff;
$textColor2: #9A9697;
$primary: #E75B12;
.homelink {
position: absolute;
right: 2rem;
top: 4rem;
transition: right 300ms linear;
a {
background-color: $bg;
color: $textColor;
line-height: 1;
font-size: 3rem;
height: 7rem;
display: flex;
align-items: center;
border-radius: 1rem;
opacity: 0.97;
svg {
transition: all 400ms linear;
}
}
&.open {
right: 20vw;
svg {
transform: scale(-1, 1);
}
}
}
.sidebar {
position: fixed;
width: 19vw;
top: 4rem;
right: -50%;
bottom: 0;
transition: all 300ms linear;
line-height: 1.25;
display: flex;
flex-direction: column;
gap: 1.5rem;
&.open {
right: 0;
}
&-content {
max-height: 100%;
overflow: auto;
background-color: $bg;
border-top-left-radius: 2rem;
border-bottom-left-radius: 2rem;
padding: 1.5rem;
color: $textColor;
font-family: 'main';
}
&-list {
}
&-list-item {
font-size: 1.875rem;
color: $textColor2;
display: flex;
input {
margin-right: 2rem;
&.checked + label {
color: $primary;
}
}
}
h2 {
font-family: 'logo';
font-size: 1.875rem;
text-align: center;
margin-bottom: 1rem;
}
p {
font-size: 1rem;
&:not(:last-child) {
margin-bottom: 1rem;
}
}
a {
background-color: darken($textColor2, 30%);
opacity: 0.97;
border-radius: 1rem;
height: 3.75rem;
color: $primary;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
font-size: 2rem;
}
}

View File

@ -1,18 +0,0 @@
<style lang="scss" scoped>
@import '../assets/home.scss';
</style>
<template>
<div class="container">
<div class="header">
Выберите шаблон (демо)
</div>
<div class="sidebar"></div>
<div class="main">
<ul>
<li>
<RouterLink to="promo">Промо</RouterLink>
</li>
</ul>
</div>
</div>
</template>

View File

@ -4,30 +4,30 @@ import {
Box3, Color, DoubleSide, Group, Mesh, PlaneGeometry,
MeshStandardMaterial, MeshStandardMaterialParameters,
Vector2, Vector3,
Object3D
} from 'three';
import { useTresContext, useSeek, useTexture, useLoop } from '@tresjs/core';
import { useGLTF } from '@tresjs/cientos'
import Env from './env.vue'
import PostProcessing from './post_processing.vue'
import { IMAGE_URL, PROMOBG, SERVER_URL, } from '../../constants'
import { usePromoSidebar } from '../../stores/promo_sidebar';
import { usePromoScene } from '../../stores/promo_scene';
import { useClickable } from '../../stores/clickable';
import { mobileAndTabletCheck } from '../../helpers';
const props = defineProps(['source', 'loaded', 'loaded_pan'])
const models = ref<model3DType[]>([])
const clickable = ref<clickableAreaType[]>([])
const clickable_items = ref<any[]>([])
const clickable_refs = ref<any[]>([])
const envVars = reactive({}) as EnvVars
const sidebar = usePromoSidebar();
const sidebar_scene = usePromoScene()
const sidebar_scene = usePromoScene();
const clickable = useClickable()
const { controls, camera, scene, raycaster, renderer } = useTresContext()
const { pause, resume } = useLoop()
const { seekByName, seekAllByName, seek } = useSeek()
@ -40,7 +40,6 @@ const { scene: pointer_pin } = await useGLTF('/pointer.glb')
const timer = ref(10)
setInterval(() => {
// console.log({ timer: timer.value })
if (timer.value > 0) {
timer.value -= 1
} else if (timer.value == 0 && !(controls.value as any).autoRotate && (controls.value as any).enabled) {
@ -87,11 +86,11 @@ const loadModels = async () => {
(controls.value as any).reset()
const sidebar_items = []
const sidebar_items = [] as PromoScene[]
clickable_items.value = []
for (let index = 0; index < data.length; index++) {
const element = data[index];
sidebar_items.push({ ...element })
const item = {} as model3DType
item.modelUrl = `${IMAGE_URL}/${element.model_file}`
@ -115,7 +114,7 @@ const loadModels = async () => {
const res = await fetch(`${SERVER_URL}/api/obj/clickable/?source=${element.id}`)
const clickable_areas = await res.json()
clickable.value.push(...clickable_areas)
clickable.list.push(...clickable_areas)
}
let c = new Color()
@ -155,10 +154,8 @@ const loadModels = async () => {
ground.name = "ground"
models.value.push({ name: 'ground', modelFile: ground })
sidebar_scene.setData(sidebar_items)
for (let index = 0; index < clickable.value.length; index++) {
const element = clickable.value[index];
for (let index = 0; index < clickable.list.length; index++) {
const element = clickable.list[index];
const find_element = seekByName(scene.value, element.object_name)
if (!find_element) continue
if (find_element && !(find_element as Group).isGroup) {
@ -167,14 +164,6 @@ const loadModels = async () => {
(find_element as Mesh).localToWorld(world_position);
const p = raw_data.min_distance * 0.05
// const plane = new PlaneGeometry(p, p, 32)
// const mesh_material = new MeshBasicMaterial({ side: DoubleSide })
// mesh_material.color = new Color('red')
// if (element.image) {
// const map = new TextureLoader().load(`${IMAGE_URL}/${element.image}`);
// mesh_material.map = map
// }
const point = pointer_pin.clone()
point.position.set(world_position.x, p * 3, world_position.z)
@ -185,9 +174,17 @@ const loadModels = async () => {
if (clickable_items.value.find(el => el.name == point.name)) continue
clickable_items.value.push(point)
clickable_refs.value.push(ref(`${element.id}_clickable`))
sidebar_items.push({
id: element.id,
name: element.name
})
}
}
sidebar_scene.name = raw_data.name;
sidebar_scene.setData(sidebar_items)
const loaded = seekByName(scene.value, 'loaded')
if (loaded) {
const box = new Box3();
@ -227,8 +224,8 @@ onAfterRender(() => {
let oldObj = [] as { uuid: string, color: Color }[]
const passShader = (obj: Mesh | Group) => {
if (obj instanceof Mesh) {
oldObj.push({ uuid: obj.uuid, color: obj.material.color })
obj.material.color = new Color(1, 0, 0)
oldObj.push({ uuid: obj.uuid, color: (obj.material as MeshStandardMaterial).color });
(obj.material as MeshStandardMaterial).color = new Color(1, 0, 0)
} else if (obj instanceof Group) {
for (let c in obj.children) {
passShader(obj.children[c])
@ -236,27 +233,7 @@ const passShader = (obj: Mesh | Group) => {
}
}
const openSidebar = (id: number) => {
const target = clickable.value.find(el => el.id == id)
if (!target) return
const sidebar_data = {
title: target.name,
description: target.description
} as PromoSidebarData
if (target?.target) {
sidebar_data.target = target.target.toString()
sidebar_data.target_name = target.target_name
}
sidebar.setData(sidebar_data)
sidebar.open()
const elements = [
seekByName(scene.value, target.name),
seekByName(scene.value, target.object_name),
].filter(Boolean)
elements.forEach((element) => {
passShader(element)
})
sidebar.open(id)
}
loadModels()
@ -316,24 +293,6 @@ const clickEvent = (event: MouseEvent) => {
openSidebar(parseInt(names[0].replace('_clickable', '')))
}
}
watch(() => sidebar_scene.list, () => {
sidebar_scene.list.forEach(element => {
const el = seekByName(scene.value, element.name)
if (!el) return
if (el.visible !== element.is_enabled) {
el.visible = element.is_enabled
}
});
}, { deep: true })
watch(() => sidebar.is_open, () => {
if (sidebar.is_open == false) {
oldObj.forEach(el => {
const obj = seek(scene.value, 'uuid', el.uuid) as Mesh
obj.material.color = el.color
})
}
})
</script>
<template>
<TresGroup name="loaded">

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { reactive, ref, watch } from 'vue';
import type { Ref } from 'vue'
import { RouterLink, useRoute } from 'vue-router';
import { useRoute } from 'vue-router';
import { Vector3 } from 'three';
import { TresCanvas } from '@tresjs/core';
@ -55,6 +55,14 @@ watch(() => route.params.target, () => {
}
}, { deep: true })
const sidebarFunc = () => {
if (sidebar.is_open) {
sidebar.close()
} else {
sidebar.open()
}
}
</script>
<template>
<div>
@ -68,8 +76,8 @@ watch(() => route.params.target, () => {
<LoadModels :source="source" :loaded="set_model_load_status" :loaded_pan="loadedPan" />
</Suspense>
</TresCanvas>
<div class="homelink">
<a href="#" @click.prevent="sidebar.open" v-if="!sidebar.is_open">
<div class="homelink" :class="[{ open: sidebar.is_open }]">
<a href="#" @click.prevent="sidebarFunc">
<i-mdi-chevron-left />
</a>
</div>
@ -93,21 +101,4 @@ watch(() => route.params.target, () => {
filter: blur(10px);
transition: all 300ms linear;
}
.homelink {
position: absolute;
right: 2rem;
top: 4rem;
a {
background-color: #2D3031;
color: #fff;
line-height: 1;
font-size: 3rem;
height: 7rem;
display: flex;
align-items: center;
border-radius: 0.5rem;
}
}
</style>

View File

@ -10,15 +10,28 @@ const sidebar_obj = ref()
const route = useRoute()
const opened_desc = ref()
const openedChange = () => {
sidebar.open(opened_desc.value)
}
</script>
<template>
<div class="sidebar" :class="[{ 'open': sidebar.is_open }]" ref="sidebar_obj">
<a href="#" @click.prevent="sidebar.close" class="sidebar-close">
<i-mdi-close />
</a>
<div class="sidebar-content">
<template v-if="!sidebar.is_open"></template>
<template v-else-if="sidebar.title">
<template v-if="!sidebar.is_open"></template>
<template v-else>
<div class="sidebar-content">
<h2>{{ sidebar_scene.name }}</h2>
<div class="sidebar-list">
<div class="sidebar-list-item" v-for="item in sidebar_scene.list">
<input type="radio" v-model=opened_desc :value="item.id" :id="item.id"
:checked="opened_desc == item.id" @change="openedChange"
:class="[{ checked: opened_desc == item.id }]" />
<label :for="item.id">{{ item.name }}</label>
</div>
</div>
</div>
<div class="sidebar-content" v-if="sidebar.title">
<h2>{{ sidebar.title }}</h2>
<template v-if="sidebar.description">
<template v-for="p in sidebar.description.split('\n')">
@ -26,134 +39,9 @@ const route = useRoute()
</template>
</template>
<RouterLink class="btn" :to="`/${route.params.item}/${sidebar.target}`" v-if="sidebar.target">
{{ sidebar.target_name }}
<i-mdi-arrow-right />
</RouterLink>
</template>
<template v-else>
<span class="sidebar-list-item" v-for="item in sidebar_scene.list">
<input type="checkbox" v-model="item.is_enabled" :id="item.name" :disabled="item.can_not_disable" />
<label :for="item.name">
<h3>{{ item.name }}</h3>
<template v-for="p in (item.description || '').split('\n')">
<p>{{ p }}</p>
</template>
</label>
</span>
</template>
</div>
</div>
</template>
</div>
</template>
<style scoped lang="scss">
.sidebar {
min-width: 20vw;
max-width: 23vw;
background-color: #fff;
position: fixed;
top: 0;
right: -50%;
bottom: 0;
transition: all 300ms linear;
line-height: 1.25;
padding: 3rem 2rem 2rem;
@media(max-width:768px) {
padding-top: 2rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
max-width: 48%;
}
&.open {
right: 0;
}
&-close {
position: absolute;
left: 1.5rem;
top: 0.5rem;
font-size: 2rem;
color: black;
@media(max-width:768px) {
left: 0.5rem;
}
}
&-content {
max-height: 100%;
overflow: auto;
}
&-list-item {
display: flex;
label {
flex-grow: 1;
margin-left: 0.25rem
}
}
h2 {
text-align: center;
font-size: 2rem;
margin: 1rem;
@media(max-width:768px) {
font-size: 1.5rem;
}
}
p,
h3 {
margin: 0.5rem 0;
}
p {
font-size: 1.25rem;
@media(max-width:768px) {
font-size: 1rem;
}
}
h3 {
font-size: 1.5rem;
font-weight: bold;
@media(max-width:768px) {
font-size: 1rem;
}
}
.btn {
display: block;
margin: 0 auto;
background: #4FD1C5;
background: linear-gradient(90deg, rgba(129, 230, 217, 1) 0%, rgba(79, 209, 197, 1) 100%);
color: #313133;
transition: .2s linear;
padding: 1rem 1.5rem;
font-size: 1.25rem;
font-weight: 700;
text-align: center;
text-decoration: none;
align-items: center;
justify-content: center;
text-transform: uppercase;
letter-spacing: 1.3px;
border-radius: 1000px;
box-shadow: 12px 12px 24px rgba(79, 209, 197, .64);
}
.btn:hover {
box-shadow: 0 0 0 2px white, inset 0 0 0 4px #3C82F8;
}
}
</style>

View File

@ -65,12 +65,7 @@ interface PromoSidebar extends PromoSidebarData {
}
interface PromoScene {
id: number
model_file: string
name: string
description?: string
parent?: number
is_enabled: boolean
can_not_disable: boolean
}
interface EnvVars {
focus: number,

View File

@ -0,0 +1,11 @@
import { defineStore } from 'pinia'
interface state {
list: clickableAreaType[]
}
export const useClickable = defineStore('clickable', {
state: () => {
return {
list: []
} as state
},
})

View File

@ -1,76 +0,0 @@
import { defineStore } from 'pinia'
import { SERVER_URL } from '../constants'
import { chunks } from '../helpers'
export const useFloorplanStore = defineStore('floorplan', {
state: () => {
return {
items: [] as { id: string, title: string }[],
title: undefined,
np_array: [] as number[][],
prepared_array: [] as number[][],
chunk_size: undefined as number | undefined,
threshold: undefined as number | undefined,
points: [] as { type: string, title: string, points: { x: number, y: number } }[]
}
},
actions: {
async getList() {
try {
const res = await fetch(`${SERVER_URL}/api/floorplan/`, {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
})
const data = await res.json()
this.items = data
// this.title = data.title
// this.np_array = data.np_field
} catch (error) {
console.log(error)
}
},
async getData(id: number) {
try {
const res = await fetch(`${SERVER_URL}/api/floorplan/${id}`, {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
})
const data = await res.json()
this.title = data.title
this.np_array = data.np_field
this.chunk_size = data.d_size || 8
this.threshold = data.d_border || 20
this.prepared_array = [...chunks(data.np_field, this.chunk_size as number)].map(line => {
const line_data = [] as any[][]
line.map((item: any) => [...chunks(item, this.chunk_size as number)]).map((item) => {
item.map((one_line, k) => {
if (!line_data[k]) line_data[k] = []
line_data[k].push(...one_line)
})
})
return line_data.map(el => {
return el.filter(e => e > 0).length > (this.threshold as number) ? 1 : 0
})
});
await this.getPoints(id)
} catch (error) {
console.log(error)
}
},
async getPoints(id: number) {
const res = await fetch(`${SERVER_URL}/api/floorplan/${id}/points`, {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
})
const data = await res.json()
this.points = data.points
}
}
})

View File

@ -1,24 +0,0 @@
import { defineStore } from 'pinia'
import { SERVER_URL } from '../constants'
export const useProductStore = defineStore('product', {
state: () => {
return {
// for initially empty lists
list: [] as ProductInfo[],
}
},
actions: {
async getData() {
try {
const res = await fetch(`${SERVER_URL}/api/products`)
const data = await res.json()
if (data.length) {
this.list = data
}
} catch (error) {
this.list = []
}
},
}
})

View File

@ -1,8 +1,15 @@
import { defineStore } from 'pinia'
type state = {
name: string,
list: PromoScene[]
}
export const usePromoScene = defineStore('promo_scene', {
state: () => {
return { list: [] as PromoScene[] }
return {
name: '',
list: []
} as state
},
actions: {
setData(data: PromoScene[]) {

View File

@ -1,4 +1,5 @@
import { defineStore } from 'pinia'
import { useClickable } from './clickable'
export const usePromoSidebar = defineStore('promo_sidebar', {
state: () => {
@ -12,9 +13,25 @@ export const usePromoSidebar = defineStore('promo_sidebar', {
} as PromoSidebar
},
actions: {
open() {
open(id: number) {
if (id) {
const clickable = useClickable()
const target = clickable.list.find(el => el.id == id)
if (!target) return
const sidebar_data = {
title: target.name,
description: target.description
} as PromoSidebarData
if (target?.target) {
sidebar_data.target = target.target.toString()
sidebar_data.target_name = target.target_name
}
this.setData(sidebar_data)
}
this.is_open = true
this.loading = true
},
setData(data: PromoSidebarData) {
this.$state = Object.assign(this.$state, data)

6
package-lock.json generated
View File

@ -1,6 +0,0 @@
{
"name": "interactive-table",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}