bx-1316-refactoring #14

Merged
ksenia_mikhailova merged 46 commits from bx-1316-refactoring into dev 2024-08-28 15:06:52 +03:00
26 changed files with 435 additions and 9488 deletions
Showing only changes of commit 74aa61e299 - Show all commits

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
# Use the official Python image with version 3.11 # Use the official Python image with version 3.11
FROM python:3.11 FROM ci.svs-tech.pro/library/python:3.11
# Set environment variables for Python and unbuffered mode # Set environment variables for Python and unbuffered mode
ENV PYTHONUNBUFFERED 1 ENV PYTHONUNBUFFERED 1
@ -12,7 +12,7 @@ ENV APP_HOME=/app
WORKDIR $APP_HOME WORKDIR $APP_HOME
# Install deb pkgs for usb # Install deb pkgs for usb
RUN apt-get update RUN apt-get update
RUN apt-get install ffmpeg libsm6 libxext6 -y # RUN apt-get install ffmpeg libsm6 libxext6 -y
# Install dependencies # Install dependencies
COPY requirements.txt /app/ COPY requirements.txt /app/
RUN pip install --upgrade pip && pip install -r requirements.txt RUN pip install --upgrade pip && pip install -r requirements.txt

View File

View File

@ -1,6 +0,0 @@
from django.contrib import admin
from .models import Product, Floorplan
# Register your models here.
admin.site.register(Product)
admin.site.register(Floorplan)

View File

@ -1,6 +0,0 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api'

View File

@ -1,31 +0,0 @@
from django.db import models
import json
from django.db import models
# Create your models here.
class Product(models.Model):
title = models.CharField(max_length=100)
description = models.TextField(default=None, null=True)
model3d = models.FileField(default=None, blank=True, null=True, upload_to="files")
image1 = models.ImageField(default=None, blank=True, null=True, upload_to="files")
image2 = models.ImageField(default=None, blank=True, null=True, upload_to="files")
image3 = models.ImageField(default=None, blank=True, null=True, upload_to="files")
def __str__(self):
return self.title
class Floorplan(models.Model):
title = models.CharField(max_length=200)
np_field = models.TextField()
d_size = models.IntegerField(null=True, blank=True)
d_border = models.IntegerField(null=True, blank=True)
paths = models.TextField()
class FloorplanPoints(models.Model):
plan = models.OneToOneField(Floorplan, on_delete=models.CASCADE, primary_key=True)
points = models.JSONField()

View File

@ -1,50 +0,0 @@
from rest_framework import routers, serializers, viewsets
from .models import Floorplan, FloorplanPoints, Product
import logging
logger = logging.getLogger("root")
class ProductSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Product
fields = [
"id",
"title",
"description",
"model3d",
"image1",
"image2",
"image3",
]
class FloorplanSerializer(serializers.ModelSerializer):
class Meta:
model = Floorplan
fields = [
"id",
"title",
"np_field",
"d_border",
"d_size",
"paths",
]
class FloorplanListSerializer(serializers.ModelSerializer):
class Meta:
model = Floorplan
fields = [
"id",
"title",
]
class FloorplanPointsSerializer(serializers.ModelSerializer):
class Meta:
model = FloorplanPoints
fields = [
"points",
"plan",
]

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,66 +0,0 @@
import io
import base64
import cv2
import numpy as np
def numpy_arr_to_zip_str(arr):
f = io.BytesIO()
np.savez_compressed(f, arr=arr)
return base64.b64encode(f.getvalue())
def numpy_zip_str_to_arr(zip_str):
f = io.BytesIO(base64.b64decode(zip_str))
return np.load(f)['arr'].tolist()
def read_image(content: bytes) -> np.ndarray:
"""
Image bytes to OpenCV image
:param content: Image bytes
:returns OpenCV image
:raises TypeError: If content is not bytes
:raises ValueError: If content does not represent an image
"""
if not isinstance(content, bytes):
raise TypeError(f"Expected 'content' to be bytes, received: {type(content)}")
image = cv2.imdecode(np.frombuffer(content, dtype=np.uint8), cv2.IMREAD_COLOR)
if image is None:
raise ValueError(f"Expected 'content' to be image bytes")
return image
def parse_image(img):
(img_h, img_w) = img.shape[:2]
t = 1920
w = t
h = int((img_h / img_w) * t)
img = cv2.resize(img, (w, h))
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray = 255 - gray
gray = cv2.threshold(gray, 250, 255, cv2.THRESH_BINARY)[1]
gray = cv2.blur(gray, (10, 5))
contours, hierarchy = cv2.findContours(gray, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
for cnt in contours:
area = cv2.contourArea(cnt)
# if area > 150000 and area < 500000:
cv2.drawContours(img, [cnt], 0, (255, 0, 0), 2)
svg_paths = []
for cnt in contours:
if len(cnt) > 80:
svg_path = "M"
for i in range(len(cnt)):
x, y = cnt[i][0]
svg_path += f"{x} {y} "
svg_paths.append(svg_path)
return {
"width": w,
"height": h,
"paths": svg_paths,
"array": gray.tolist(),
"b64": numpy_arr_to_zip_str(gray),
}

View File

@ -1,134 +0,0 @@
import json
from rest_framework.parsers import JSONParser, MultiPartParser
from rest_framework.views import APIView
from rest_framework import status
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from api.tracer import parse_image, read_image, numpy_zip_str_to_arr
from .serializers import (
FloorplanListSerializer,
FloorplanPointsSerializer,
FloorplanSerializer,
ProductSerializer,
)
from .models import Floorplan, FloorplanPoints, Product
import logging
logger = logging.getLogger("root")
class Products(APIView):
def get(self, request):
tasks = Product.objects.all()
serializer = ProductSerializer(tasks, many=True)
return JsonResponse(serializer.data, safe=False)
def post(self, request):
data = JSONParser().parse(request)
serializer = ProductSerializer(data=data)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, status=201)
return JsonResponse(serializer.errors, status=400)
class FloorplanView(APIView):
parser_classes = (
MultiPartParser,
JSONParser,
)
def post(self, request):
try:
file = request.FILES["demo"]
logger.info(file.__dict__)
res = parse_image(read_image(file.read()))
serializer = FloorplanSerializer(
data={
"title": file.name,
"np_field": res["b64"].decode(),
"paths": json.dumps(res["paths"]),
}
)
if serializer.is_valid():
serializer.save()
return JsonResponse(
data={
"response": {
"np_field": res["array"],
"paths": res["paths"],
}
},
status=201,
)
else:
logger.info(serializer.errors)
return JsonResponse(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(e)
raise e
def get(self, request, id=None):
try:
if id is not None:
item = Floorplan.objects.get(id=id)
serializer = FloorplanSerializer(item, many=False)
data = serializer.data
data["np_field"] = numpy_zip_str_to_arr(data["np_field"])
return JsonResponse(data, safe=False)
else:
items = Floorplan.objects.only("id", "title").all()
serializer = FloorplanListSerializer(items, many=True)
return JsonResponse(serializer.data, safe=False)
except Exception as e:
logger.error(e)
raise e
def patch(self, request, id):
try:
if id is not None:
item = Floorplan.objects.get(id=id)
serializer = FloorplanSerializer(item, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, status=status.HTTP_200_OK)
else:
logger.info(serializer.errors)
return JsonResponse(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
else:
return JsonResponse("No item", status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(e)
raise e
class FloorplanPointsView(APIView):
def get(self, request, id):
if FloorplanPoints.objects.filter(plan=id).exists():
points = FloorplanPoints.objects.get(plan=id)
serializer = FloorplanPointsSerializer(points, many=False)
return JsonResponse(serializer.data, safe=False)
else:
return JsonResponse(
"No item", safe=False, status=status.HTTP_400_BAD_REQUEST
)
def post(self, request, id):
data = JSONParser().parse(request)
data["plan"] = id
floorplapoints_object = get_object_or_404(FloorplanPoints, plan=id)
serializer = FloorplanPointsSerializer(floorplapoints_object, data=data, partial=True)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, status=status.HTTP_201_CREATED)
else:
return JsonResponse(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

View File

@ -84,7 +84,6 @@ INSTALLED_APPS = [
"crispy_forms", "crispy_forms",
"crispy_bootstrap4", "crispy_bootstrap4",
"colorfield", "colorfield",
"api",
"frontImages", "frontImages",
"object", "object",
] ]

View File

@ -18,7 +18,6 @@ from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from django.conf.urls.static import static from django.conf.urls.static import static
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from api import views
from rest_framework import routers from rest_framework import routers
from frontImages import views as frontimg_views from frontImages import views as frontimg_views
@ -33,8 +32,4 @@ router.register(r'api/obj/clickable', object_views.ClickableAreaViewSet)
urlpatterns = [ urlpatterns = [
path('', include(router.urls)), path('', include(router.urls)),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("api/products", csrf_exempt(views.Products.as_view())),
path("api/floorplan/", csrf_exempt(views.FloorplanView.as_view())),
path("api/floorplan/<str:id>", csrf_exempt(views.FloorplanView.as_view())),
path("api/floorplan/<str:id>/points", csrf_exempt(views.FloorplanPointsView.as_view())),
] + static('/files', document_root='files') ] + static('/files', document_root='files')

View File

@ -1,4 +1,4 @@
FROM node:21 FROM ci.svs-tech.pro/library/node:21
RUN mkdir -p /src RUN mkdir -p /src

View File

@ -21,7 +21,7 @@ declare module 'vue' {
IMdiPagePreviousOutline: typeof import('~icons/mdi/page-previous-outline')['default'] IMdiPagePreviousOutline: typeof import('~icons/mdi/page-previous-outline')['default']
IMdiShop: typeof import('~icons/mdi/shop')['default'] IMdiShop: typeof import('~icons/mdi/shop')['default']
IMdiVideo3d: typeof import('~icons/mdi/video3d')['default'] IMdiVideo3d: typeof import('~icons/mdi/video3d')['default']
Item: typeof import('./src/components/Floorplan/item.vue')['default'] Item: typeof import('./src/components/Promo/item.vue')['default']
Load_models: typeof import('./src/components/Promo/load_models.vue')['default'] Load_models: typeof import('./src/components/Promo/load_models.vue')['default']
Main: typeof import('./src/components/Promo/main.vue')['default'] Main: typeof import('./src/components/Promo/main.vue')['default']
ModelItem: typeof import('./src/components/Promo/modelItem.vue')['default'] ModelItem: typeof import('./src/components/Promo/modelItem.vue')['default']

View File

@ -1,17 +1,4 @@
<script setup lang="ts">
import { RouterLink } from 'vue-router';
</script>
<template> <template>
<div class="nav">
<RouterLink to="/projects"><i-mdi-shop /></RouterLink>
<RouterLink to="/game"><i-mdi-hexagon-outline /></RouterLink>
<RouterLink to="/promo"><i-mdi-monitor-screenshot /></RouterLink>
<span style="flex-grow:1"></span>
<RouterLink to="/"><i-mdi-home /></RouterLink>
</div>
<Suspense> <Suspense>
<RouterView /> <RouterView />
</Suspense> </Suspense>

View File

@ -1,25 +0,0 @@
<style lang="scss" scoped></style>
<script setup lang="ts">
import { onMounted, } from 'vue';
import { useFloorplanStore } from '../../stores/floorplan';
const floorplan = useFloorplanStore()
onMounted(async () => {
await floorplan.getList()
})
</script>
<template>
<div class="container">
<ul>
<li v-for="item in floorplan.items">
<RouterLink :to="`/floorplan/${item.id}`">
{{ item.title }}
</RouterLink>
</li>
</ul>
</div>
</template>
<style scoped>
</style>

View File

@ -1,150 +0,0 @@
<style lang="scss" scoped></style>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import PF, { Grid } from 'pathfinding'
import { useFloorplanStore } from '../../stores/floorplan';
import { useRoute } from 'vue-router';
import { random_сolor } from '../../helpers';
type PathItem = { path: string, unwalkable: boolean, x: number, y: number }
const floorplan = useFloorplanStore()
const canvasElement = ref();
const context = ref();
const grid = ref<Grid>()
const startPoint = ref<{ x: number, y: number }>({ x: 25, y: 40 })
const endPoint = ref<{ x: number, y: number }>()
const startToEndPath = ref<number[][] | undefined>([])
const plan = useFloorplanStore()
const paths = ref<PathItem[]>([])
const finder = new PF.BreadthFirstFinder();
const nextFrame = () => new Promise(resolve => requestAnimationFrame(resolve));
const route = useRoute()
const newDraw = async () => {
endPoint.value = undefined
startToEndPath.value = undefined
context.value = canvasElement.value?.getContext('2d') || undefined;
const lines = plan.np_array
const c = random_сolor()
for (let indexY = 0; indexY < lines.length; indexY++) {
const line = lines[indexY];
for (let indexX = 0; indexX < line.length; indexX++) {
const point = line[indexX];
if (canvasElement.value && context.value && point > 0) {
context.value.fillStyle = c.replace(')', ',0.5)').replace('rgb', 'rgba')
context.value.fillRect(indexX, indexY, 1, 1)
}
}
if (indexY % 10 == 0) {
await nextFrame()
}
}
const quantum_lines = plan.prepared_array
const chunkSize = plan.chunk_size || 8
quantum_lines.forEach((line, indexY) => {
const targetY = indexY * chunkSize
line.forEach((point, indexX) => {
const targetX = indexX * chunkSize
paths.value.push({
path: `M${targetX} ${targetY} ${targetX + chunkSize} ${targetY} ${targetX + chunkSize} ${targetY + chunkSize} ${targetX} ${targetY + chunkSize}Z`,
unwalkable: !!point,
x: indexX,
y: indexY,
})
})
})
grid.value = new PF.Grid(plan.prepared_array.map(y => y.map(x => x > 0 ? 1 : 0)))
}
const findPath = async () => {
// filders.forEach((finder,i) => {
console.time(`findpath`)
if (!endPoint.value) return
const localPath: number[][] = finder.findPath(
Math.round(startPoint.value.x),
Math.round(startPoint.value.y),
Math.round(endPoint.value.x),
Math.round(endPoint.value.y),
(grid.value?.clone() as Grid)
);
startToEndPath.value = localPath
console.timeEnd(`findpath`)
// });
}
const setPointSvg = (item: { x: number, y: number }) => {
// startToEndPath.value = []
const startP = floorplan.points.find(el => el.type === 'start')
if (!startP) return
startPoint.value = { x: startP.points.x, y: startP.points.y }
endPoint.value = { x: item.x, y: item.y }
findPath()
}
onMounted(async () => {
await floorplan.getData(parseInt(route.params.id as string))
newDraw()
})
const cw = 1920
const ch = 800
</script>
<template>
<div class="container" style="display: flex; justify-content: center; align-items: center; flex-direction: column;">
<div class="svg-container" style="position: relative">
<canvas ref="canvasElement" :width="cw" :height="ch"></canvas>
<svg ref="svgElement" :width="cw" :height="ch" style="position: absolute; top: 0; right: 0;">
<path
v-for="item in paths.filter(item => floorplan.points.find(el => el.points.x == item.x && el.points.y == item.y))"
:d="item.path" @click="setPointSvg(item)" :class="[
{ 'endPoint': !!(floorplan.points.find(el => el.type == 'start' && el.points.x == item.x && el.points.y == item.y)) },
{ 'startPoint': !!(floorplan.points.find(el => el.type.indexOf('cabinet') !== -1 && el.points.x === item.x && el.points.y === item.y)) },
{ 'pathPoint': (startToEndPath && startToEndPath.find((el: number[]) => el[0] == item.x && el[1] == item.y)) },
]">
</path>
<path
v-for="item in paths.filter(item => (startToEndPath || []).find(el => el[0] == item.x && el[1] == item.y))"
:d="item.path" @click="setPointSvg(item)" :class="[
{ 'endPoint': !!(floorplan.points.find(el => el.type == 'start' && el.points.x == item.x && el.points.y == item.y)) },
{ 'startPoint': !!(floorplan.points.find(el => el.type.indexOf('cabinet') !== -1 && el.points.x === item.x && el.points.y === item.y)) },
{ 'pathPoint': (startToEndPath && startToEndPath.find((el: number[]) => el[0] == item.x && el[1] == item.y)) },
]">
</path>
</svg>
</div>
<div class="buttons">
<template v-for="item in floorplan.points">
<span v-if="item.type !== 'start'" @click="setPointSvg({ x: item.points.x, y: item.points.y })">
{{ item.title }}
</span>
</template>
</div>
</div>
</template>
<style scoped>
svg path.endPoint {
fill: blue;
}
svg path.startPoint {
fill: lawngreen;
}
svg path.pathPoint {
fill: gold;
}
.buttons {
display: flex;
gap: 1rem;
}
</style>

View File

@ -1,151 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue';
import { UseDraggable } from '@vueuse/components';
import { type Position } from '@vueuse/core';
import { random_сolor, shuffle_array, getRandomIntInclusive } from '../helpers';
import icon1 from '~icons/mdi/square';
import icon1_o from '~icons/mdi/square-outline';
import icon2 from '~icons/mdi/triangle';
import icon2_o from '~icons/mdi/triangle-outline';
import icon3 from '~icons/mdi/circle';
import icon3_o from '~icons/mdi/circle-outline';
import icon4 from '~icons/mdi/hexagon';
import icon4_o from '~icons/mdi/hexagon-outline';
const icons_list = [
icon1,
icon1_o,
icon2,
icon2_o,
icon3,
icon3_o,
icon4,
icon4_o,
]
const colors = ref([
random_сolor(),
random_сolor(),
random_сolor(),
random_сolor(),
])
const icons = [
{
id: 1, fill: 0, outline: 1,
},
{
id: 2, fill: 2, outline: 3,
},
{
id: 3, fill: 4, outline: 5,
},
{
id: 4, fill: 6, outline: 7,
},
]
const source_icons = ref(shuffle_array(icons))
const target_icons = ref(shuffle_array(icons))
const active_drag = ref<string | null>(null)
const dragStart = (_: Position, event: PointerEvent) => {
const element = event.currentTarget
if (element instanceof HTMLElement && element.dataset.id) {
active_drag.value = element.dataset.id
}
}
const dragEnd = (position: Position, _: PointerEvent) => {
const target = document.elementsFromPoint(position.x, position.y).find(el => el.className == 'target-item')
if (target instanceof HTMLElement && target.dataset.id) {
const targetId = target.dataset.id
const sourceId = active_drag.value
if (targetId == sourceId) {
success.value.push(parseInt(targetId))
}
}
}
const componentKey = ref(0)
const success = ref<number[]>([])
const updateGame = () => {
success.value = []
componentKey.value += 1
source_icons.value = shuffle_array(icons)
target_icons.value = shuffle_array(icons)
colors.value = [random_сolor(), random_сolor(), random_сolor(), random_сolor()]
}
const coor = (n: number) => {
let x = 0
let y = 0
const groups = [
{ x: [100, 1400], y: [100, 300] },
{ x: [1400, 1600], y: [100, 800] },
{ x: [100, 1600], y: [700, 800] },
{ x: [100, 200], y: [100, 800] },
]
const d = groups[n]
x = getRandomIntInclusive(d.x[0], d.x[1])
y = getRandomIntInclusive(d.y[0], d.y[1])
return { x, y }
}
</script>
<template>
<div class="game" :key="componentKey">
<div class="target">
<span class="target-item" v-for="item in target_icons" :data-id="item.id">
<component :is="success.includes(item.id) ? icons_list[item.fill] : icons_list[item.outline]"
:style="{ 'color': colors[item.id - 1] }"></component>
</span>
</div>
<UseDraggable v-for="item in source_icons" class="source-item" :data-id="item.id"
:initial-value="coor(getRandomIntInclusive(1, 3))" :on-start="dragStart" :on-end="dragEnd">
<component v-if="!success.includes(item.id)" :is="icons_list[item.fill]"
:style="{ 'color': colors[item.id - 1] }" :ref="item.ref"></component>
</UseDraggable>
<div class="links" v-if="success.length >= icons.length">
<RouterLink to="/">На главную</RouterLink>
<RouterLink to="/game" @click="updateGame">Еще раз</RouterLink>
</div>
</div>
</template>
<style scoped lans="scss">
.game {
position: fixed;
height: 100vh;
width: 100vw;
display: flex;
flex-direction: column;
justify-content: space-around;
}
.target {
display: flex;
width: 100%;
justify-content: center;
gap: 2rem;
}
.target-item {
font-size: 12rem;
}
.source-item {
font-size: 7rem;
position: absolute;
}
.links {
display: flex;
flex-direction: column;
font-size: 5rem;
align-items: center;
gap: 1rem;
}
.links a {
text-decoration: none;
color: black;
}
</style>

View File

@ -9,21 +9,9 @@
<div class="sidebar"></div> <div class="sidebar"></div>
<div class="main"> <div class="main">
<ul> <ul>
<li>
<RouterLink to="projects">Проекты</RouterLink>
</li>
<li>
<RouterLink to="game">Игра</RouterLink>
</li>
<li> <li>
<RouterLink to="promo">Промо</RouterLink> <RouterLink to="promo">Промо</RouterLink>
</li> </li>
<li>
<a href="https://timesheet.kustarshina.ru/">Табель рабочего времени</a>
</li>
<li>
<a href="http://zoo.svs-tech.pro/">Билетная система зоопарка</a>
</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -1,137 +0,0 @@
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue';
import { ModelFbx } from 'vue-3d-model';
import type { UseSwipeDirection } from '@vueuse/core';
import { useSwipe } from '@vueuse/core';
import Fireworks from '@fireworks-js/vue';
import type { FireworksOptions } from '@fireworks-js/vue'
import RandomIcon from '../components/RandomIcon.vue';
import { useProductStore } from '../stores/product';
import type { ProductInfo } from '../stores/product';
const IMAGE_URL = import.meta.env.VITE_IMAGE_URL ?? window.location.origin
type StateType = {
active_product?: ProductInfo
show_model: boolean,
show_fireworks: boolean
}
const products = useProductStore()
const state: StateType = reactive({
active_product: undefined,
show_model: false,
show_fireworks: false
})
const reset = () => {
state.active_product = undefined
state.show_model = false
state.show_fireworks = false
}
const setActive = (id: number) => {
state.active_product = products.list.find(el => el.id == id)
state.show_model = false
state.show_fireworks = false
}
const toggleShowCanvas = () => {
state.show_model = !state.show_model
}
const target = ref<HTMLElement | null>(null)
useSwipe(target, {
onSwipeEnd(_: TouchEvent, direction: UseSwipeDirection) {
if (state.show_model) {
return
}
const index = products.list.findIndex(el => el.id == state.active_product?.id)
if (direction === 'right') {
if (index == products.list.length - 1) {
setActive(products.list[0].id)
} else {
setActive(products.list[index + 1].id)
}
} else if (direction === 'left') {
if (index == 0) {
setActive(products.list[products.list.length - 1].id)
} else {
setActive(products.list[index - 1].id)
}
}
}
})
const fw = ref<InstanceType<typeof Fireworks>>()
const options = ref<FireworksOptions>({
lineStyle: 'square',
intensity: 50,
lineWidth: {
explosion: { min: 7, max: 10 },
trace: { min: 7, max: 10 },
}
})
onMounted(async () => {
products.getData()
})
</script>
<style lang="scss" scoped>
@import '../assets/projects.scss';
</style>
<template>
<div class="container">
<div class="sidebar">
<ul class="menu">
<li v-for="item in products.list">
<RandomIcon v-if="item.id === state.active_product?.id" />
<a @click.stop.prevent="setActive(item.id)" :href="item.id.toString()">
{{ item.title }}
</a>
</li>
</ul>
</div>
<div class="header"><span class="logo-header" @click="reset">Проекты Кустарщины</span></div>
<div class="main product" v-if="state.active_product" ref="target">
<div class="product-image" v-if="!state.show_model && state.active_product.image1">
<img :style="{
clipPath: `polygon(
0% 10%, 10% 0%,
90% 0%, 100% 10%,
100% 90%, 90% 100%,
10% 100%, 0% 90%
)`}" :src="`${IMAGE_URL}${state.active_product.image1}`" />
</div>
<div class="product-description" v-if="!state.show_model">
{{ state.active_product.description }}
</div>
<a class="product-model-icon" v-if="state.active_product.model3d" @click.stop.prevent="toggleShowCanvas">
<i-mdi-video-3d v-if="!state.show_model" />
<i-mdi-file v-else />
</a>
<model-fbx v-if="state.show_model && state.active_product.model3d" class="product-model"
:src="`${IMAGE_URL}/${state.active_product.model3d.replace('/back', '')}`"
:backgroundAlpha="0"></model-fbx>
</div>
<div class="main" v-else>
<img class="logo-img" src="../assets/logo_color.png"
@click="() => { state.show_fireworks = !state.show_fireworks }" />
<Fireworks ref="fw" v-if="state.show_fireworks" :autostart="true" :options="options" :style="{
top: 0,
left: 0,
width: '100%',
height: '100%',
position: 'fixed',
zIndex: 0,
pointerEvents: 'none',
}" />
</div>
</div>
</template>

View File

@ -1,13 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, onUnmounted, reactive, ref, watch } from 'vue'; import { onMounted, onUnmounted, reactive, ref, watch } from 'vue';
import { import {
Box3, CircleGeometry, Color, DoubleSide, Group, Mesh, MeshBasicMaterial, Box3, Color, DoubleSide, Group, Mesh, MeshBasicMaterial,
MeshStandardMaterial, MeshStandardMaterial,
MeshStandardMaterialParameters, MeshStandardMaterialParameters,
PlaneGeometry, SpriteMaterial, TextureLoader, Vector2, Vector3, PlaneGeometry, SpriteMaterial, TextureLoader, Vector2, Vector3,
} from 'three'; } from 'three';
import { useTresContext, useSeek, useRenderLoop, useTexture, useLoop } from '@tresjs/core'; import { useTresContext, useSeek, useTexture, useLoop } from '@tresjs/core';
import { useGLTF } from '@tresjs/cientos' import { useGLTF } from '@tresjs/cientos'
import Env from './env.vue' import Env from './env.vue'
@ -15,6 +15,7 @@ import Env from './env.vue'
import { IMAGE_URL, PROMOBG, SERVER_URL, } from '../../constants' import { IMAGE_URL, PROMOBG, SERVER_URL, } from '../../constants'
import { usePromoSidebar } from '../../stores/promo_sidebar'; import { usePromoSidebar } from '../../stores/promo_sidebar';
import { usePromoScene } from '../../stores/promo_scene'; import { usePromoScene } from '../../stores/promo_scene';
import { mobileAndTabletCheck } from '../../helpers';
const props = defineProps(['source', 'loaded', 'loaded_pan']) const props = defineProps(['source', 'loaded', 'loaded_pan'])
@ -27,33 +28,33 @@ const sidebar_scene = usePromoScene()
const { controls, camera, scene, raycaster, renderer } = useTresContext() const { controls, camera, scene, raycaster, renderer } = useTresContext()
const { pause, resume } = useLoop() const { pause, resume } = useLoop()
const { seekByName, seekAllByName } = useSeek() const { seekByName, seekAllByName } = useSeek()
const envVars = reactive({}) as { const envVars = reactive({}) as EnvVars
focus: number,
hdr_gainmap?: string,
hdr_json?: string,
hdr_webp?: string,
clear_color?: string,
env_displacementmap?: string,
env_normalmap?: string
}
const groundTexture = await useTexture({ const groundTexture = await useTexture({
displacementMap: '/ground_displacement.jpg', displacementMap: '/ground_displacement.jpg',
}) })
const timer = ref(10) const timer = ref(10)
let int: any; 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) {
pause()
if (controls.value) {
camera.value?.position.set(10, (controls.value as any).minDistance * 0.75, (controls.value as any).minDistance);
camera.value?.lookAt(0, 0, 0);
(controls.value as any).autoRotate = true;
(controls.value as any).autoRotateSpeed = 1;
}
resume()
}
}, 1000);
// renderer.value.capabilities.maxTextures = 4 // renderer.value.capabilities.maxTextures = 4
renderer.value.capabilities.maxTextureSize = 512 // renderer.value.capabilities.maxTextureSize = 512
renderer.value.capabilities.precision = 'lowp' renderer.value.capabilities.precision = 'lowp'
const mobileAndTabletCheck = () => {
let check = false;
(function (a) { if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4))) check = true; })(navigator.userAgent || navigator.vendor || window.opera);
return check;
};
const loadModels = async () => { const loadModels = async () => {
const res = await fetch(`${SERVER_URL}/api/obj/scene/${props.source}`) const res = await fetch(`${SERVER_URL}/api/obj/scene/${props.source}`)
const raw_data = await res.json() as scene3D const raw_data = await res.json() as scene3D
@ -74,14 +75,18 @@ const loadModels = async () => {
const data = raw_data.elements const data = raw_data.elements
if (!controls.value) return; if (!controls.value) return;
camera.value?.position.set(1, 1, 1);
controls.value.enabled = false; controls.value.enabled = false;
// console.log(mobileAndTabletCheck() ? raw_data.min_distance * 0.5 : raw_data.min_distance);
(controls.value as any).minDistance = mobileAndTabletCheck() ? raw_data.min_distance * 0.5 : raw_data.min_distance; (controls.value as any).minDistance = mobileAndTabletCheck() ? raw_data.min_distance * 0.5 : raw_data.min_distance;
(controls.value as any).maxDistance = raw_data.max_distance; (controls.value as any).maxDistance = raw_data.max_distance;
(controls.value as any)._needsUpdate = true; (controls.value as any)._needsUpdate = true;
(controls.value as any).update() (controls.value as any).update()
camera.value?.position.set(10, (controls.value as any).minDistance * 0.75, (controls.value as any).minDistance);
camera.value?.lookAt(0, 0, 0);
(controls.value as any).reset()
const sidebar_items = [] const sidebar_items = []
clickable_items.value = [] clickable_items.value = []
for (let index = 0; index < data.length; index++) { for (let index = 0; index < data.length; index++) {
@ -201,23 +206,10 @@ const loadModels = async () => {
controls.value.enabled = true; controls.value.enabled = true;
props.loaded(false) props.loaded(false)
clearInterval(int)
timer.value = 10 timer.value = 10
int = setInterval(() => { if (controls.value && (controls.value as any).autoRotate) {
if (timer.value > 0) { (controls.value as any).autoRotate = false;
timer.value -= 1
} else if (timer.value == 0 && !controls.value.autoRotate) {
pause()
camera.value?.position.set(
controls.value.minDistance * 0.5,
controls.value.minDistance * 0.5,
controls.value.minDistance
);
(controls.value as any).autoRotate = true;
(controls.value as any).autoRotateSpeed = 1;
resume()
} }
}, 1000)
} }
const { onAfterRender } = useLoop() const { onAfterRender } = useLoop()
@ -261,22 +253,33 @@ watch(() => props.source, () => {
onMounted(() => { onMounted(() => {
document.addEventListener('click', clickEvent) document.addEventListener('click', clickEvent)
document.addEventListener('click', stopTimer) document.addEventListener('click', stopTimer)
document.addEventListener('contextmenu', stopTimer)
document.addEventListener('touchstart', stopTimer) document.addEventListener('touchstart', stopTimer)
document.addEventListener('touchend', stopTimer)
document.addEventListener('touchmove', stopTimer)
if (sidebar.is_open) { if (sidebar.is_open) {
sidebar.close() sidebar.close()
} }
}) })
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('click', clickEvent) document.removeEventListener('click', clickEvent)
document.removeEventListener('click', stopTimer) document.removeEventListener('click', stopTimer)
document.removeEventListener('contextmenu', stopTimer)
document.removeEventListener('touchstart', stopTimer) document.removeEventListener('touchstart', stopTimer)
document.removeEventListener('touchend', stopTimer)
document.removeEventListener('touchmove', stopTimer)
}) })
const pointer = reactive({ x: 0, y: 0 }) const pointer = reactive({ x: 0, y: 0 })
const stopTimer = () => { const stopTimer = () => {
timer.value = 10; timer.value = 10;
if (controls.value.autoRotate) { if (controls.value && (controls.value as any).autoRotate) {
controls.value.autoRotate = false; (controls.value as any).autoRotate = false;
} }
} }
const clickEvent = (event: MouseEvent) => { const clickEvent = (event: MouseEvent) => {

View File

@ -1,14 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { RouterLink } from 'vue-router'; import { RouterLink } from 'vue-router';
import { onClickOutside } from '@vueuse/core'
import { usePromoSidebar } from '../../stores/promo_sidebar'; import { usePromoSidebar } from '../../stores/promo_sidebar';
import { usePromoScene } from '../../stores/promo_scene'; import { usePromoScene } from '../../stores/promo_scene';
const sidebar = usePromoSidebar() const sidebar = usePromoSidebar()
const sidebar_scene = usePromoScene() const sidebar_scene = usePromoScene()
const sidebar_obj = ref() const sidebar_obj = ref()
// onClickOutside(sidebar_obj, () => sidebar.close())
</script> </script>
<template> <template>
@ -34,7 +32,7 @@ const sidebar_obj = ref()
<input type="checkbox" v-model="item.is_enabled" :id="item.name" :disabled="item.can_not_disable" /> <input type="checkbox" v-model="item.is_enabled" :id="item.name" :disabled="item.can_not_disable" />
<label :for="item.name"> <label :for="item.name">
<h3>{{ item.name }}</h3> <h3>{{ item.name }}</h3>
<template v-for="p in item.description.split('\n')"> <template v-for="p in (item.description || '').split('\n')">
<p>{{ p }}</p> <p>{{ p }}</p>
</template> </template>
</label> </label>
@ -57,7 +55,8 @@ const sidebar_obj = ref()
padding: 3rem 2rem 2rem; padding: 3rem 2rem 2rem;
@media(max-width:768px) { @media(max-width:768px) {
padding-top: 2rem;; padding-top: 2rem;
;
padding-left: 0.75rem; padding-left: 0.75rem;
padding-right: 0.75rem; padding-right: 0.75rem;
max-width: 48%; max-width: 48%;

View File

@ -30,3 +30,9 @@ export function* chunks<T>(arr: T[], n: number): Generator<T[], void> {
yield arr.slice(i, i + n); yield arr.slice(i, i + n);
} }
} }
export const mobileAndTabletCheck = () => {
let check = false;
(function (a) { if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4))) check = true; })(navigator.userAgent || navigator.vendor || window.opera);
return check;
};

13
front/src/index.d.ts vendored
View File

@ -34,6 +34,9 @@ interface element3DType {
max_distance?: number, max_distance?: number,
is_enabled: boolean is_enabled: boolean
can_not_disable: boolean can_not_disable: boolean
x_pos: number
y_pos: number
z_pos: number
} }
interface model3DType { interface model3DType {
modelUrl?: string, modelUrl?: string,
@ -67,4 +70,14 @@ interface PromoScene {
description?: string description?: string
parent?: number parent?: number
is_enabled: boolean is_enabled: boolean
can_not_disable: boolean
}
interface EnvVars {
focus: number,
hdr_gainmap?: string,
hdr_json?: string,
hdr_webp?: string,
clear_color?: string,
env_displacementmap?: string,
env_normalmap?: string
} }

View File

@ -6,20 +6,12 @@ import './assets/main.scss'
import App from './App.vue' import App from './App.vue'
import Home from './components/Home.vue' import Home from './components/Home.vue'
import Projects from './components/Projects.vue'
import Game from './components/Game.vue'
import Floorplan from './components/Floorplan/index.vue'
import FloorplanItem from './components/Floorplan/item.vue'
import Promo from './components/Promo/index.vue' import Promo from './components/Promo/index.vue'
import PromoMain from './components/Promo/main.vue' import PromoMain from './components/Promo/main.vue'
import PromoItem from './components/Promo/item.vue' import PromoItem from './components/Promo/item.vue'
const routes = [ const routes = [
{ path: '/', component: Home }, { path: '/', component: Home },
{ path: '/projects', component: Projects },
{ path: '/game', component: Game },
{ path: '/floorplan', component: Floorplan },
{ path: '/floorplan/:id', component: FloorplanItem },
{ path: '/promo', component: Promo }, { path: '/promo', component: Promo },
{ path: '/promo/:page', component: PromoMain }, { path: '/promo/:page', component: PromoMain },
{ path: '/promo/:page/:target', component: PromoMain }, { path: '/promo/:page/:target', component: PromoMain },