Compare commits

...

13 Commits

Author SHA1 Message Date
Kseninia Mikhaylova f37b457007 v-html 2025-02-11 15:30:57 +03:00
Kseninia Mikhaylova ee2878d9a4 host 2025-02-10 17:03:20 +03:00
Kseninia Mikhaylova ee973695d7 host 2025-02-10 17:00:31 +03:00
Kseninia Mikhaylova b84241d8de Admin 2025-02-10 16:56:01 +03:00
aarizona 5ec685374d use url 2024-09-10 11:26:00 +03:00
aarizona 0a801c4188 add file 2024-09-10 11:12:42 +03:00
aarizona 50c78fd219 remove fog totally 2024-09-09 16:47:28 +03:00
aarizona 19026731f6 docker fix 2024-09-06 20:48:09 +03:00
aarizona 1219204f45 mass glb import 2024-09-06 20:45:10 +03:00
Kseninia Mikhaylova 6cbb63b4e8 sidebar btn 2024-09-06 16:54:02 +03:00
Kseninia Mikhaylova 1329678fe6 controls update 2024-09-06 16:46:12 +03:00
Kseninia Mikhaylova e9ebc96aac close sidebar fully 2024-09-06 16:35:00 +03:00
Kseninia Mikhaylova 8872674ba6 revert visible 2024-09-06 16:21:21 +03:00
15 changed files with 365 additions and 65 deletions

1
.gitignore vendored
View File

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

View File

@ -26,6 +26,7 @@ class Item(models.Model):
slug = models.SlugField(unique=True)
images = models.ManyToManyField(Image)
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()
def __str__(self):

View File

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

View File

@ -1,15 +1,152 @@
from django.contrib import admin
from django.db import models
from import_export.admin import ImportExportModelAdmin
from .models import ClickableArea, Element3D, Environment, Scene3D
class Scene3DAdmin(ImportExportModelAdmin, admin.ModelAdmin):
filter_horizontal = ("elements",)
# Админ-класс для Scene3D
@admin.register(Scene3D)
class Scene3DAdmin(admin.ModelAdmin):
list_display = ('name', 'description', 'element_count')
search_fields = ('name', 'description')
autocomplete_fields = ('elements',) # Используем автодополнение
class ImportExportBtnAdmin(ImportExportModelAdmin, admin.ModelAdmin):
pass
# Метод для подсчета количества связанных элементов
@admin.display(description='Количество элементов')
def element_count(self, obj):
return obj.elements.count()
admin.site.register(Environment, ImportExportBtnAdmin)
admin.site.register(Scene3D, Scene3DAdmin)
admin.site.register(Element3D, ImportExportBtnAdmin)
admin.site.register(ClickableArea, ImportExportBtnAdmin)
@admin.register(Environment)
class EnvironmentAdmin(admin.ModelAdmin):
list_display = ("id", "clear_color_display", "clear_color_to_display", "file_count")
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

@ -0,0 +1,69 @@
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,50 +15,6 @@ 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):
max_width = 512
max_height = 512
@ -68,19 +24,129 @@ def maximum_size_validator(image):
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):
name = models.CharField(
"Название",
verbose_name="Название",
max_length=255,
help_text="Название кликабельной области",
)
description = models.TextField(
"Описание",
verbose_name="Описание",
help_text="Описание кликабельной области",
)
target = models.ForeignKey(
Scene3D,
"Scene3D", # Предполагается, что Scene3D определен где-то выше или в том же файле
on_delete=models.PROTECT,
related_name="clickable_areas",
blank=True,
@ -88,15 +154,19 @@ class ClickableArea(models.Model):
help_text="На какую сцену ведет клик",
)
source = models.ForeignKey(
Element3D,
"Element3D", # Предполагается, что Element3D определен где-то выше или в том же файле
on_delete=models.PROTECT,
help_text="В каком элементе искать object_name",
)
object_name = models.CharField(
"Название объекта",
verbose_name="Название объекта",
max_length=255,
help_text="Имя mesh или group в элементе 3D",
)
def __str__(self):
return self.name
class Meta:
verbose_name = "Кликабельная область" # Человекочитаемое имя одной записи
verbose_name_plural = "Кликабельные области" # Человекочитаемое имя множественного числа

View File

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

View File

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

View File

@ -50,7 +50,7 @@ const loadEnv = async () => {
const c = new Color()
c.set(props.clear_color || PROMOBG)
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 }
new Color(props.clear_color).getHSL(hsl_value);

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import { defineStore } from 'pinia'
import { useClickable } from './clickable'
import { usePromoScene } from './promo_scene'
export const usePromoSidebar = defineStore('promo_sidebar', {
state: () => {
@ -47,13 +48,18 @@ export const usePromoSidebar = defineStore('promo_sidebar', {
// this.target = undefined;
// this.loading = true;
this.is_open = false;
this.accordions = []
// this.accordions = [];
}
},
closeBtn() {
this.$reset;
this.is_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) {
if (name == 'obj' && this.accordions.includes('clickable')) this.toggleAccordion('clickable')

View File

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