Compare commits

..

5 Commits

15 changed files with 65 additions and 365 deletions

1
.gitignore vendored
View File

@ -2,7 +2,6 @@
files/* files/*
postgres_data/* postgres_data/*
export_images/* export_images/*
glb_import/*
.vscode/ .vscode/
__pycache__/ __pycache__/
poetry.lock poetry.lock

View File

@ -26,7 +26,6 @@ class Item(models.Model):
slug = models.SlugField(unique=True) slug = models.SlugField(unique=True)
images = models.ManyToManyField(Image) images = models.ManyToManyField(Image)
scene_3d = models.ForeignKey(Scene3D, on_delete=models.RESTRICT) scene_3d = models.ForeignKey(Scene3D, on_delete=models.RESTRICT)
file = models.FileField(blank=True, null=True, upload_to=group_based_upload_to)
is_front = models.BooleanField() is_front = models.BooleanField()
def __str__(self): def __str__(self):

View File

@ -11,7 +11,6 @@ class ImageSerializer(serializers.ModelSerializer):
class ItemSerializer(serializers.ModelSerializer): class ItemSerializer(serializers.ModelSerializer):
images = ImageSerializer(many=True) images = ImageSerializer(many=True)
file = serializers.FileField(use_url=False)
class Meta: class Meta:
lookup_field = 'slug' lookup_field = 'slug'

View File

@ -1,152 +1,15 @@
from django.contrib import admin from django.contrib import admin
from django.db import models
from import_export.admin import ImportExportModelAdmin from import_export.admin import ImportExportModelAdmin
from .models import ClickableArea, Element3D, Environment, Scene3D from .models import ClickableArea, Element3D, Environment, Scene3D
# Админ-класс для Scene3D class Scene3DAdmin(ImportExportModelAdmin, admin.ModelAdmin):
@admin.register(Scene3D) filter_horizontal = ("elements",)
class Scene3DAdmin(admin.ModelAdmin):
list_display = ('name', 'description', 'element_count')
search_fields = ('name', 'description')
autocomplete_fields = ('elements',) # Используем автодополнение
# Метод для подсчета количества связанных элементов class ImportExportBtnAdmin(ImportExportModelAdmin, admin.ModelAdmin):
@admin.display(description='Количество элементов') pass
def element_count(self, obj):
return obj.elements.count()
admin.site.register(Environment, ImportExportBtnAdmin)
@admin.register(Environment) admin.site.register(Scene3D, Scene3DAdmin)
class EnvironmentAdmin(admin.ModelAdmin): admin.site.register(Element3D, ImportExportBtnAdmin)
list_display = ("id", "clear_color_display", "clear_color_to_display", "file_count") admin.site.register(ClickableArea, ImportExportBtnAdmin)
list_filter = ("clear_color", "clear_color_to")
search_fields = ("id",)
readonly_fields = ("file_preview",)
# Метод для отображения цвета clear_color
@admin.display(description="Цвет очистки (начальный)", empty_value="-")
def clear_color_display(self, obj):
if obj.clear_color:
return f'<div style="background-color:{obj.clear_color}; width:50px; height:15px;"></div>'
return "-"
clear_color_display.allow_tags = True
# Метод для отображения цвета clear_color_to
@admin.display(description="Цвет очистки (конечный)", empty_value="-")
def clear_color_to_display(self, obj):
if obj.clear_color_to:
return f'<div style="background-color:{obj.clear_color_to}; width:50px; height:15px;"></div>'
return "-"
clear_color_to_display.allow_tags = True
# Метод для подсчета количества загруженных файлов
@admin.display(description="Количество файлов", empty_value="0")
def file_count(self, obj):
return sum(
bool(getattr(obj, field.name))
for field in obj._meta.fields
if isinstance(field, models.FileField)
)
# Метод для предпросмотра файлов
@admin.display(description="Предпросмотр файлов")
def file_preview(self, obj):
preview_html = []
if obj.hdr_gainmap:
preview_html.append(
f'<a href="{obj.hdr_gainmap.url}" target="_blank">Просмотр Gainmap</a>'
)
if obj.hdr_json:
preview_html.append(
f'<a href="{obj.hdr_json.url}" target="_blank">Просмотр JSON</a>'
)
if obj.hdr_webp:
preview_html.append(
f'<a href="{obj.hdr_webp.url}" target="_blank">Просмотр WEBP</a>'
)
return "<br>".join(preview_html) or "-"
file_preview.allow_tags = True
@admin.register(Element3D)
class Element3DAdmin(admin.ModelAdmin):
list_display = (
"name",
"model_file_display",
"is_enabled",
"can_not_disable",
"position_display",
"scenes_count",
)
list_filter = ("is_enabled", "can_not_disable")
search_fields = ("name", "description")
readonly_fields = ("model_file_preview",)
# Метод для отображения пути к файлу модели
@admin.display(description="Файл модели", empty_value="-")
def model_file_display(self, obj):
return obj.model_file.name if obj.model_file else "-"
# Метод для отображения позиции элемента
@admin.display(description="Позиция (X, Y, Z)")
def position_display(self, obj):
return f"({obj.x_pos}, {obj.y_pos}, {obj.z_pos})"
# Метод для подсчета количества связанных сцен
@admin.display(description="Количество сцен", empty_value="0")
def scenes_count(self, obj):
return obj.scene3d_set.count()
# Метод для предпросмотра файла модели
@admin.display(description="Предпросмотр файла")
def model_file_preview(self, obj):
if obj.model_file:
return f'<a href="{obj.model_file.url}" target="_blank">Просмотр файла</a>'
return "-"
model_file_preview.allow_tags = True
# Декоратор для регистрации модели ClickableArea
@admin.register(ClickableArea)
class ClickableAreaAdmin(admin.ModelAdmin):
list_display = (
"name",
"description_shortened",
"source_element",
"target_scene",
"object_name",
)
list_filter = ("source__name", "target__name")
search_fields = (
"name",
"description",
"object_name",
"source__name",
"target__name",
)
autocomplete_fields = ("source", "target")
# Метод для отображения укороченного описания
@admin.display(description="Описание", empty_value="-")
def description_shortened(self, obj):
return (
(obj.description[:50] + "...")
if len(obj.description) > 50
else obj.description
)
# Метод для отображения связанного элемента (source)
@admin.display(description="Элемент 3D", empty_value="-")
def source_element(self, obj):
return obj.source.name if obj.source else "-"
# Метод для отображения связанной сцены (target)
@admin.display(description="Сцена", empty_value="-")
def target_scene(self, obj):
return obj.target.name if obj.target else "-"

View File

@ -1,69 +0,0 @@
import os
import glob
from django.core.management.base import BaseCommand
from django.core.files import File
import logging
from object.models import Scene3D, Element3D
logger = logging.getLogger(__name__) # Инициализация логгера
class Command(BaseCommand):
help = "Import GLB files into the database and associate them with a specific Scene3D object."
def handle(self, *args, **options):
logger.info("Starting the import process...")
# Определение корневой директории
base_dir = os.path.dirname(os.path.abspath(__file__)) # Получаем путь к текущему файлу команды
root_directory = os.path.join(base_dir, "../../data") # Переходим на уровень выше и указываем папку data
root_directory = os.path.normpath(root_directory) # Нормализуем путь для кроссплатформенности
if not os.path.exists(root_directory):
logger.error(f"The directory {root_directory} does not exist.")
return
logger.info(f"Using root directory: {root_directory}")
# Поиск файлов .glb
try:
files = glob.glob("*.glb", recursive=True, root_dir=root_directory)
if not files:
logger.warning("No .glb files found in the specified directory.")
return
logger.info(f"Found {len(files)} .glb files: {', '.join(files)}")
except Exception as e:
logger.error(f"Error while searching for .glb files: {e}")
return
# Получение объекта Scene3D
try:
hv = Scene3D.objects.get(id=56)
logger.info(f"Retrieved Scene3D object with ID 56: {hv}")
except Scene3D.DoesNotExist:
logger.error("Scene3D object with ID 56 does not exist.")
return
except Exception as e:
logger.error(f"Error while retrieving Scene3D object: {e}")
return
# Обработка каждого файла
for f in files:
file_path = os.path.join(root_directory, f)
logger.info(f"Processing file: {file_path}")
try:
with open(file_path, 'rb') as file:
el = Element3D(name=f)
el.model_file = File(file, f)
el.save()
logger.info(f"Successfully saved Element3D object: {el}")
hv.elements.add(el)
logger.info(f"Added Element3D {el} to Scene3D {hv}. Total elements count: {hv.elements.count()}")
except FileNotFoundError:
logger.error(f"File not found: {file_path}")
except Exception as e:
logger.error(f"Error while processing file {f}: {e}")
logger.info("Import process completed successfully.")

View File

@ -15,6 +15,50 @@ def group_based_upload_to(instance, filename):
) )
class Environment(models.Model):
clear_color = ColorField(blank=True, null=True)
clear_color_to = ColorField(blank=True, null=True)
hdr_gainmap = models.FileField(
upload_to=group_based_upload_to, blank=True, null=True
)
hdr_json = models.FileField(upload_to=group_based_upload_to, blank=True, null=True)
hdr_webp = models.FileField(upload_to=group_based_upload_to, blank=True, null=True)
class Element3D(models.Model):
model_file = models.FileField(upload_to=group_based_upload_to)
name = models.CharField(max_length=255)
description = models.TextField(blank=True, null=True)
is_enabled = models.BooleanField(default=True)
can_not_disable = models.BooleanField(default=False)
x_pos = models.IntegerField(default=0)
y_pos = models.IntegerField(default=0)
z_pos = models.IntegerField(default=0)
def __str__(self):
return self.name
class Scene3D(models.Model):
name = models.CharField(max_length=120)
description = models.TextField(blank=True, null=True)
elements = models.ManyToManyField(Element3D)
env = models.ForeignKey(Environment, models.RESTRICT, blank=True, null=True)
min_distance = models.IntegerField(
default=10,
validators=[MinValueValidator(1), MaxValueValidator(600)],
)
max_distance = models.IntegerField(
default=20,
validators=[MinValueValidator(2), MaxValueValidator(1000)],
)
def __str__(self):
return self.name
def maximum_size_validator(image): def maximum_size_validator(image):
max_width = 512 max_width = 512
max_height = 512 max_height = 512
@ -24,129 +68,19 @@ def maximum_size_validator(image):
raise ValidationError("Height or Width is larger than what is allowed") raise ValidationError("Height or Width is larger than what is allowed")
class Environment(models.Model):
clear_color = ColorField(
blank=True,
null=True,
verbose_name="Цвет очистки (начальный)"
)
clear_color_to = ColorField(
blank=True,
null=True,
verbose_name="Цвет очистки (конечный)"
)
hdr_gainmap = models.FileField(
upload_to=group_based_upload_to,
blank=True,
null=True,
verbose_name="HDR Gainmap файл"
)
hdr_json = models.FileField(
upload_to=group_based_upload_to,
blank=True,
null=True,
verbose_name="HDR JSON файл"
)
hdr_webp = models.FileField(
upload_to=group_based_upload_to,
blank=True,
null=True,
verbose_name="HDR WEBP файл"
)
def __str__(self):
return f"Среда #{self.id}"
class Meta:
verbose_name = "Среда"
verbose_name_plural = "Среды"
class Element3D(models.Model):
model_file = models.FileField(
upload_to=group_based_upload_to,
verbose_name="Файл модели"
)
name = models.CharField(
max_length=255,
verbose_name="Название элемента"
)
description = models.TextField(
blank=True,
null=True,
verbose_name="Описание элемента"
)
is_enabled = models.BooleanField(
default=True,
verbose_name="Включен"
)
can_not_disable = models.BooleanField(
default=False,
verbose_name="Невозможно отключить"
)
x_pos = models.IntegerField(
default=0,
verbose_name="Позиция X"
)
y_pos = models.IntegerField(
default=0,
verbose_name="Позиция Y"
)
z_pos = models.IntegerField(
default=0,
verbose_name="Позиция Z"
)
def __str__(self):
return self.name
class Meta:
verbose_name = "Элемент 3D"
verbose_name_plural = "Элементы 3D"
class Scene3D(models.Model):
name = models.CharField(max_length=120, verbose_name="Название сцены")
description = models.TextField(blank=True, null=True, verbose_name="Описание сцены")
elements = models.ManyToManyField("Element3D", verbose_name="Элементы 3D")
env = models.ForeignKey(
"Environment",
on_delete=models.RESTRICT,
blank=True,
null=True,
verbose_name="Среда",
)
min_distance = models.IntegerField(
default=10,
validators=[MinValueValidator(1), MaxValueValidator(600)],
verbose_name="Минимальное расстояние",
)
max_distance = models.IntegerField(
default=20,
validators=[MinValueValidator(2), MaxValueValidator(1000)],
verbose_name="Максимальное расстояние",
)
def __str__(self):
return self.name
class Meta:
verbose_name = "Сцена 3D"
verbose_name_plural = "Сцены 3D"
class ClickableArea(models.Model): class ClickableArea(models.Model):
name = models.CharField( name = models.CharField(
verbose_name="Название", "Название",
max_length=255, max_length=255,
help_text="Название кликабельной области", help_text="Название кликабельной области",
) )
description = models.TextField( description = models.TextField(
verbose_name="Описание", "Описание",
help_text="Описание кликабельной области", help_text="Описание кликабельной области",
) )
target = models.ForeignKey( target = models.ForeignKey(
"Scene3D", # Предполагается, что Scene3D определен где-то выше или в том же файле Scene3D,
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name="clickable_areas", related_name="clickable_areas",
blank=True, blank=True,
@ -154,19 +88,15 @@ class ClickableArea(models.Model):
help_text="На какую сцену ведет клик", help_text="На какую сцену ведет клик",
) )
source = models.ForeignKey( source = models.ForeignKey(
"Element3D", # Предполагается, что Element3D определен где-то выше или в том же файле Element3D,
on_delete=models.PROTECT, on_delete=models.PROTECT,
help_text="В каком элементе искать object_name", help_text="В каком элементе искать object_name",
) )
object_name = models.CharField( object_name = models.CharField(
verbose_name="Название объекта", "Название объекта",
max_length=255, max_length=255,
help_text="Имя mesh или group в элементе 3D", help_text="Имя mesh или group в элементе 3D",
) )
def __str__(self): def __str__(self):
return self.name return self.name
class Meta:
verbose_name = "Кликабельная область" # Человекочитаемое имя одной записи
verbose_name_plural = "Кликабельные области" # Человекочитаемое имя множественного числа

View File

@ -37,7 +37,6 @@ services:
volumes: volumes:
- ./.env:/app/.env - ./.env:/app/.env
- ./files:/app/files - ./files:/app/files
- ./glb_import:/app/object/management/commands/data
networks: networks:
- dev - dev

View File

@ -807,7 +807,6 @@
"version": "3.9.0", "version": "3.9.0",
"resolved": "https://registry.npmjs.org/@tresjs/cientos/-/cientos-3.9.0.tgz", "resolved": "https://registry.npmjs.org/@tresjs/cientos/-/cientos-3.9.0.tgz",
"integrity": "sha512-LAtMveKlecKvWh7TNWdwEs3nQUYMLqz9DZy0YhSZ6OVTfL2vevx2K4sH9744UME8OedUf4fkFFkX4OWQRHaDRQ==", "integrity": "sha512-LAtMveKlecKvWh7TNWdwEs3nQUYMLqz9DZy0YhSZ6OVTfL2vevx2K4sH9744UME8OedUf4fkFFkX4OWQRHaDRQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@tresjs/core": "3.9.0", "@tresjs/core": "3.9.0",
"@vueuse/core": "^10.9.0", "@vueuse/core": "^10.9.0",

View File

@ -50,7 +50,7 @@ const loadEnv = async () => {
const c = new Color() const c = new Color()
c.set(props.clear_color || PROMOBG) c.set(props.clear_color || PROMOBG)
renderer.value.setClearColor(c) renderer.value.setClearColor(c)
// scene.value.fog = new Fog(c, props.focus * k.start, props.focus * k.end) scene.value.fog = new Fog(c, props.focus * k.start, props.focus * k.end)
const hsl_value = { h: 0, s: 0, l: 0 } const hsl_value = { h: 0, s: 0, l: 0 }
new Color(props.clear_color).getHSL(hsl_value); new Color(props.clear_color).getHSL(hsl_value);

View File

@ -58,7 +58,7 @@ const sidebarFunc = () => {
if (sidebar.is_open) { if (sidebar.is_open) {
sidebar.close() sidebar.close()
} else { } else {
sidebar.is_open = true sidebar.open()
} }
} }

View File

@ -130,7 +130,6 @@ const loadModels = async () => {
item.modelFile = loaded_scene item.modelFile = loaded_scene
item.id = element.id item.id = element.id
item.name = element.name item.name = element.name
console.log(item)
if (!element.is_enabled) { if (!element.is_enabled) {
item.modelFile.visible = false item.modelFile.visible = false
@ -246,7 +245,6 @@ watch(() => props.source, () => {
} }
console.log('props change') console.log('props change')
sidebar.close() sidebar.close()
sidebar.closeBtn()
} else { } else {
renderer.value.dispose() renderer.value.dispose()
} }
@ -296,7 +294,7 @@ onBeforeRender(() => {
const dis_to_cam = camera.value?.position.distanceTo(el.value[0].position); const dis_to_cam = camera.value?.position.distanceTo(el.value[0].position);
if (dis_to_cam) { if (dis_to_cam) {
const scaling = (0.66 * dis_to_cam) / 100 const scaling = (0.5 * dis_to_cam) / 100
el.value[0].children[0].scale.set(scaling, scaling, scaling); el.value[0].children[0].scale.set(scaling, scaling, scaling);
el.value[0].updateMatrixWorld() el.value[0].updateMatrixWorld()
} }
@ -337,8 +335,6 @@ onBeforeRender(() => {
} }
} }
(controls.value as any).update()
}) })
watch(() => targetDistance.min, () => { watch(() => targetDistance.min, () => {

View File

@ -64,7 +64,7 @@ watch(() => sidebar.id_clickable, () => {
<div class="sidebar-accordion-content" v-if="sidebar.isAccOpen('desc')"> <div class="sidebar-accordion-content" v-if="sidebar.isAccOpen('desc')">
<template <template
v-for="p in (sidebar.description || sidebar_scene.description || '').replace(/(\n|\r)+/g, '\n').split('\n')"> v-for="p in (sidebar.description || sidebar_scene.description || '').replace(/(\n|\r)+/g, '\n').split('\n')">
<p v-html="p"></p> <p>{{ p }}</p>
</template> </template>
<RouterLink :to="`/${route.params.item}/${sidebar.target}`" v-if="sidebar.target"> <RouterLink :to="`/${route.params.item}/${sidebar.target}`" v-if="sidebar.target">
Перейти Перейти

View File

@ -4,14 +4,12 @@ type state = {
name?: string, name?: string,
description?: string, description?: string,
clickable: PromoScene[], clickable: PromoScene[],
_visible: { id: number, is_enabled: boolean }[],
visible: PromoScene[], visible: PromoScene[],
} }
export const usePromoScene = defineStore('promo_scene', { export const usePromoScene = defineStore('promo_scene', {
state: () => { state: () => {
return { return {
clickable: [], clickable: [],
_visible: [],
visible: [], visible: [],
} as state } as state
}, },
@ -24,10 +22,7 @@ export const usePromoScene = defineStore('promo_scene', {
this.clickable = data this.clickable = data
}, },
setVisible(data: PromoScene[]) { setVisible(data: PromoScene[]) {
this._visible = data.slice(0).map(el => { this.visible = data
return { id: el.id, is_enabled: el.is_enabled ?? true }
})
this.visible = data.slice(0)
} }
} }
}) })

View File

@ -1,6 +1,5 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { useClickable } from './clickable' import { useClickable } from './clickable'
import { usePromoScene } from './promo_scene'
export const usePromoSidebar = defineStore('promo_sidebar', { export const usePromoSidebar = defineStore('promo_sidebar', {
state: () => { state: () => {
@ -48,18 +47,13 @@ export const usePromoSidebar = defineStore('promo_sidebar', {
// this.target = undefined; // this.target = undefined;
// this.loading = true; // this.loading = true;
this.is_open = false; this.is_open = false;
// this.accordions = []; this.accordions = []
} }
}, },
closeBtn() { closeBtn() {
this.$reset; this.$reset;
this.is_open = false; this.is_open = false;
this.is_btn_open = false; this.is_btn_open = false;
const sidebar_scene = usePromoScene()
sidebar_scene.visible.map(el => {
el.is_enabled = (sidebar_scene._visible.find(item => item.id == el.id) ?? { is_enabled: true }).is_enabled
})
}, },
toggleAccordion(name: string, newState = null) { toggleAccordion(name: string, newState = null) {
if (name == 'obj' && this.accordions.includes('clickable')) this.toggleAccordion('clickable') if (name == 'obj' && this.accordions.includes('clickable')) this.toggleAccordion('clickable')

View File

@ -22,8 +22,4 @@ export default defineConfig({
svgLoader(), svgLoader(),
], ],
assetsInclude: ['**/*.fbx','**/*.glb', '**/*.gltf', '**/*.hdr'], assetsInclude: ['**/*.fbx','**/*.glb', '**/*.gltf', '**/*.hdr'],
preview: {
host: "demo.kustarshina.ru",
allowedHosts: ["demo.kustarshina.ru",],
}
}) })