fix: synchronize product visuals and apply Ozon light theme
Stop React RigidBody props from fighting Rapier mesh sync on playback ticks, and convert the shell/scene to the centralized Ozon light palette. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -8,12 +8,13 @@ import { getRenderedItemDimensions } from '../../domain/physicalLayout';
|
|||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import RealItemModel, { isProductAssetReady } from './RealItemModel';
|
import RealItemModel, { isProductAssetReady } from './RealItemModel';
|
||||||
import { ItemVerificationOverlay } from './RealModelVerification';
|
import { ItemVerificationOverlay } from './RealModelVerification';
|
||||||
|
import { CATEGORY_COLORS, INDUSTRIAL_PALETTE } from '../../domain/industrialTheme';
|
||||||
|
|
||||||
const COLORS = {
|
const COLORS = {
|
||||||
B: '#16a34a',
|
B: CATEGORY_COLORS.B,
|
||||||
C: '#ea580c',
|
C: CATEGORY_COLORS.C,
|
||||||
D: '#7c3aed',
|
D: CATEGORY_COLORS.D,
|
||||||
sensorAccent: '#3b82f6',
|
sensorAccent: INDUSTRIAL_PALETTE.sensorAccent,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Base real-model materials per SKU (Stage 1 §20 — basic, form-revealing). */
|
/** Base real-model materials per SKU (Stage 1 §20 — basic, form-revealing). */
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* C/D redirection is contact-only against kinematic CAD diverter colliders.
|
* C/D redirection is contact-only against kinematic CAD diverter colliders.
|
||||||
* No junction setTranslation / route-specific lateral impulses.
|
* No junction setTranslation / route-specific lateral impulses.
|
||||||
*/
|
*/
|
||||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { memo, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { useFrame } from '@react-three/fiber';
|
import { useFrame } from '@react-three/fiber';
|
||||||
import {
|
import {
|
||||||
@@ -62,7 +62,7 @@ function colliderDensity(profile: ReturnType<typeof getProductPhysicsProfile>):
|
|||||||
return profile.massKg / Math.max(volume, 1e-6);
|
return profile.massKg / Math.max(volume, 1e-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PhysicalPlaybackItemPhysics = memo(function PhysicalPlaybackItemPhysics({
|
function PhysicalPlaybackItemPhysicsInner({
|
||||||
caseData,
|
caseData,
|
||||||
elapsedMs,
|
elapsedMs,
|
||||||
slotIndex = 0,
|
slotIndex = 0,
|
||||||
@@ -134,7 +134,7 @@ export const PhysicalPlaybackItemPhysics = memo(function PhysicalPlaybackItemPhy
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [caseData.id, profile.productId]);
|
}, [caseData.id, profile.productId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
bodyIdentity.current = `${caseData.id}:${itemId}`;
|
bodyIdentity.current = `${caseData.id}:${itemId}`;
|
||||||
authority.current = isFault ? 'fault_kinematic' : 'preparing';
|
authority.current = isFault ? 'fault_kinematic' : 'preparing';
|
||||||
phaseRef.current = 'preparing';
|
phaseRef.current = 'preparing';
|
||||||
@@ -148,6 +148,7 @@ export const PhysicalPlaybackItemPhysics = memo(function PhysicalPlaybackItemPhy
|
|||||||
setSpawned(ready);
|
setSpawned(ready);
|
||||||
const body = bodyRef.current;
|
const body = bodyRef.current;
|
||||||
if (body) {
|
if (body) {
|
||||||
|
// Imperative spawn pose — never reapplied via React RigidBody props.
|
||||||
body.setBodyType(RigidBodyType.KinematicPositionBased, false);
|
body.setBodyType(RigidBodyType.KinematicPositionBased, false);
|
||||||
body.setLinvel({ x: 0, y: 0, z: 0 }, true);
|
body.setLinvel({ x: 0, y: 0, z: 0 }, true);
|
||||||
body.setAngvel({ x: 0, y: 0, z: 0 }, true);
|
body.setAngvel({ x: 0, y: 0, z: 0 }, true);
|
||||||
@@ -350,16 +351,17 @@ export const PhysicalPlaybackItemPhysics = memo(function PhysicalPlaybackItemPhy
|
|||||||
if (elapsedMs < 0) return null;
|
if (elapsedMs < 0) return null;
|
||||||
|
|
||||||
const density = colliderDensity(profile);
|
const density = colliderDensity(profile);
|
||||||
const enabledRotations: [boolean, boolean, boolean] = [
|
const enabledRotations = useMemo<[boolean, boolean, boolean]>(
|
||||||
!profile.lockRotationX,
|
() => [!profile.lockRotationX, !profile.lockRotationY, !profile.lockRotationZ],
|
||||||
!profile.lockRotationY,
|
[profile.lockRotationX, profile.lockRotationY, profile.lockRotationZ],
|
||||||
!profile.lockRotationZ,
|
);
|
||||||
];
|
|
||||||
|
|
||||||
|
// Initial pose props are mount-only (component is memoized against elapsedMs
|
||||||
|
// for non-fault cases). Never pass `type` — imperative setBodyType owns it.
|
||||||
|
// World transform authority after spawn: Rapier body → R3F object3D sync.
|
||||||
return (
|
return (
|
||||||
<RigidBody
|
<RigidBody
|
||||||
ref={bodyRef}
|
ref={bodyRef}
|
||||||
type="kinematicPosition"
|
|
||||||
colliders={false}
|
colliders={false}
|
||||||
friction={profile.beltFriction}
|
friction={profile.beltFriction}
|
||||||
restitution={profile.restitution}
|
restitution={profile.restitution}
|
||||||
@@ -371,6 +373,8 @@ export const PhysicalPlaybackItemPhysics = memo(function PhysicalPlaybackItemPhy
|
|||||||
enabledRotations={enabledRotations}
|
enabledRotations={enabledRotations}
|
||||||
position={spawnPose.position}
|
position={spawnPose.position}
|
||||||
rotation={spawnPose.rotation}
|
rotation={spawnPose.rotation}
|
||||||
|
name={`product-body-${itemId}`}
|
||||||
|
userData={{ productId: itemId, bodyIdentity: bodyIdentity.current }}
|
||||||
>
|
>
|
||||||
{spawned && profile.collider.type === 'cuboid' && (
|
{spawned && profile.collider.type === 'cuboid' && (
|
||||||
<CuboidCollider
|
<CuboidCollider
|
||||||
@@ -398,7 +402,7 @@ export const PhysicalPlaybackItemPhysics = memo(function PhysicalPlaybackItemPhy
|
|||||||
rotation={profile.collider.axis === 'x' ? [0, 0, Math.PI / 2] : undefined}
|
rotation={profile.collider.axis === 'x' ? [0, 0, Math.PI / 2] : undefined}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<group visible={spawned}>
|
<group name="ProductVisualGroup" visible={spawned}>
|
||||||
<ItemVisualContent
|
<ItemVisualContent
|
||||||
caseData={caseData}
|
caseData={caseData}
|
||||||
phase={pose.phase}
|
phase={pose.phase}
|
||||||
@@ -411,4 +415,25 @@ export const PhysicalPlaybackItemPhysics = memo(function PhysicalPlaybackItemPhy
|
|||||||
</group>
|
</group>
|
||||||
</RigidBody>
|
</RigidBody>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block playback-tick re-renders for normal (non-fault) products so React
|
||||||
|
* RigidBody props cannot fight Rapier's mesh sync every frame.
|
||||||
|
*/
|
||||||
|
export const PhysicalPlaybackItemPhysics = memo(
|
||||||
|
PhysicalPlaybackItemPhysicsInner,
|
||||||
|
(prev, next) => {
|
||||||
|
if (prev.caseData.id !== next.caseData.id) return false;
|
||||||
|
if (prev.slotIndex !== next.slotIndex) return false;
|
||||||
|
if (prev.castShadow !== next.castShadow) return false;
|
||||||
|
if (prev.verifySku !== next.verifySku) return false;
|
||||||
|
if (prev.caseData.faultType !== next.caseData.faultType) return false;
|
||||||
|
if (prev.jitter !== next.jitter) return false;
|
||||||
|
// Fault kinematics still need elapsedMs; physical products do not.
|
||||||
|
if (next.caseData.faultType) {
|
||||||
|
return prev.elapsedMs === next.elapsedMs;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -114,12 +114,12 @@ const COLORS = {
|
|||||||
supports: INDUSTRIAL_PALETTE.plastic,
|
supports: INDUSTRIAL_PALETTE.plastic,
|
||||||
motor: INDUSTRIAL_PALETTE.metalDark,
|
motor: INDUSTRIAL_PALETTE.metalDark,
|
||||||
sensorAccent: INDUSTRIAL_PALETTE.sensorAccent,
|
sensorAccent: INDUSTRIAL_PALETTE.sensorAccent,
|
||||||
sensorActive: '#60a5fa',
|
sensorActive: INDUSTRIAL_PALETTE.sensorActive,
|
||||||
gateFrame: INDUSTRIAL_PALETTE.metal,
|
gateFrame: INDUSTRIAL_PALETTE.metal,
|
||||||
routeB: INDUSTRIAL_PALETTE.routeB,
|
routeB: INDUSTRIAL_PALETTE.routeB,
|
||||||
routeC: INDUSTRIAL_PALETTE.routeC,
|
routeC: INDUSTRIAL_PALETTE.routeC,
|
||||||
routeD: INDUSTRIAL_PALETTE.routeD,
|
routeD: INDUSTRIAL_PALETTE.routeD,
|
||||||
itemShadow: '#3a4a5a',
|
itemShadow: INDUSTRIAL_PALETTE.metalDark,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ITEM_MATERIALS: Record<string, { color: string; roughness: number; metalness?: number }> = {
|
const ITEM_MATERIALS: Record<string, { color: string; roughness: number; metalness?: number }> = {
|
||||||
@@ -505,10 +505,13 @@ function ZoneMarker({ position, label, color, active }: {
|
|||||||
</mesh>
|
</mesh>
|
||||||
<Html position={[0, 0.15, 0]} center>
|
<Html position={[0, 0.15, 0]} center>
|
||||||
<div style={{
|
<div style={{
|
||||||
color: active ? color : '#64748b',
|
color: active ? color : INDUSTRIAL_PALETTE.text,
|
||||||
fontSize: '20px',
|
fontSize: '20px',
|
||||||
fontWeight: 800,
|
fontWeight: 800,
|
||||||
textShadow: active ? `0 0 8px ${color}` : 'none',
|
padding: '2px 8px',
|
||||||
|
borderRadius: 8,
|
||||||
|
background: 'rgba(255,255,255,0.88)',
|
||||||
|
border: `1px solid ${active ? color : INDUSTRIAL_PALETTE.gridCell}`,
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
}}>
|
}}>
|
||||||
{label}
|
{label}
|
||||||
@@ -553,7 +556,16 @@ function BReceiverBin({ active }: { active: boolean }) {
|
|||||||
<meshStandardMaterial color={metal} roughness={0.65} metalness={0.35} />
|
<meshStandardMaterial color={metal} roughness={0.65} metalness={0.35} />
|
||||||
</mesh>
|
</mesh>
|
||||||
<Html position={[centerX, floorY + wallHeight + 0.18, centerZ]} center>
|
<Html position={[centerX, floorY + wallHeight + 0.18, centerZ]} center>
|
||||||
<div style={{ color: active ? COLORS.routeB : '#64748b', fontSize: '20px', fontWeight: 800, userSelect: 'none' }}>B</div>
|
<div style={{
|
||||||
|
color: active ? COLORS.routeB : INDUSTRIAL_PALETTE.text,
|
||||||
|
fontSize: '20px',
|
||||||
|
fontWeight: 800,
|
||||||
|
padding: '2px 8px',
|
||||||
|
borderRadius: 8,
|
||||||
|
background: 'rgba(255,255,255,0.88)',
|
||||||
|
border: `1px solid ${active ? COLORS.routeB : INDUSTRIAL_PALETTE.gridCell}`,
|
||||||
|
userSelect: 'none',
|
||||||
|
}}>B</div>
|
||||||
</Html>
|
</Html>
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
@@ -582,10 +594,13 @@ function RollCage({ position, label, color, active, shadows = false }: {
|
|||||||
{/* Label */}
|
{/* Label */}
|
||||||
<Html position={[0, height + 0.15, 0]} center>
|
<Html position={[0, height + 0.15, 0]} center>
|
||||||
<div style={{
|
<div style={{
|
||||||
color: active ? color : '#64748b',
|
color: active ? color : INDUSTRIAL_PALETTE.text,
|
||||||
fontSize: '18px',
|
fontSize: '18px',
|
||||||
fontWeight: 800,
|
fontWeight: 800,
|
||||||
textShadow: active ? `0 0 8px ${color}` : 'none',
|
padding: '2px 8px',
|
||||||
|
borderRadius: 8,
|
||||||
|
background: 'rgba(255,255,255,0.88)',
|
||||||
|
border: `1px solid ${active ? color : INDUSTRIAL_PALETTE.gridCell}`,
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
}}>
|
}}>
|
||||||
{label}
|
{label}
|
||||||
@@ -1025,8 +1040,9 @@ function ContinuousScene({
|
|||||||
const protoShadows = proto ? (proto && stage0!.shadows) : shadowsEnabled;
|
const protoShadows = proto ? (proto && stage0!.shadows) : shadowsEnabled;
|
||||||
const protoCamera = proto && stage0!.camera;
|
const protoCamera = proto && stage0!.camera;
|
||||||
const shotOverride = protoCamera && stage0!.shot ? shotToPhaseCategory(stage0!.shot) : null;
|
const shotOverride = protoCamera && stage0!.shot ? shotToPhaseCategory(stage0!.shot) : null;
|
||||||
// Stage 2: premium industrial dark environment is the default look.
|
// Ozon light presentation environment is the default product look.
|
||||||
const darkBg = proto ? stage0!.darkBackground : true;
|
// Prototype stage0 may still request a dark background explicitly.
|
||||||
|
const darkBg = proto ? stage0!.darkBackground : false;
|
||||||
|
|
||||||
const cameraHighlight = shouldHighlightCamera(phase);
|
const cameraHighlight = shouldHighlightCamera(phase);
|
||||||
const showScan = shouldShowScanEffect(phase);
|
const showScan = shouldShowScanEffect(phase);
|
||||||
@@ -1174,13 +1190,18 @@ function ContinuousScene({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Background: industrial dark product canvas (equipment must dominate) */}
|
{/* Background: Ozon light presentation canvas (equipment stays readable) */}
|
||||||
<color attach="background" args={[darkBg ? INDUSTRIAL_PALETTE.backgroundDark : '#070d16']} />
|
<color
|
||||||
<fog attach="fog" args={[darkBg ? INDUSTRIAL_PALETTE.backgroundDark : '#070d16', 13, 26]} />
|
attach="background"
|
||||||
|
args={[darkBg ? INDUSTRIAL_PALETTE.backgroundDark : INDUSTRIAL_PALETTE.background]}
|
||||||
|
/>
|
||||||
|
<fog
|
||||||
|
attach="fog"
|
||||||
|
args={[darkBg ? INDUSTRIAL_PALETTE.backgroundDark : INDUSTRIAL_PALETTE.background, 16, 32]}
|
||||||
|
/>
|
||||||
|
|
||||||
{proto ? (
|
{proto ? (
|
||||||
<>
|
<>
|
||||||
{/* Cinematic rig: very low ambient, strong key, soft fill, cool rim */}
|
|
||||||
<ambientLight intensity={stage0!.ambient} />
|
<ambientLight intensity={stage0!.ambient} />
|
||||||
<hemisphereLight args={['#223148', '#0b1220', 0.3]} />
|
<hemisphereLight args={['#223148', '#0b1220', 0.3]} />
|
||||||
<directionalLight
|
<directionalLight
|
||||||
@@ -1197,21 +1218,18 @@ function ContinuousScene({
|
|||||||
shadow-camera-far={25}
|
shadow-camera-far={25}
|
||||||
shadow-bias={-0.0004}
|
shadow-bias={-0.0004}
|
||||||
/>
|
/>
|
||||||
{/* Fill — keeps shadowed side readable */}
|
|
||||||
{stage0!.fill && <directionalLight position={[-5, 6, -3]} intensity={0.35} />}
|
{stage0!.fill && <directionalLight position={[-5, 6, -3]} intensity={0.35} />}
|
||||||
{/* Rim — cheap back light for edge separation */}
|
|
||||||
{stage0!.rim && <directionalLight position={[2, 5, -8]} intensity={0.7} color="#bcd7ff" />}
|
{stage0!.rim && <directionalLight position={[2, 5, -8]} intensity={0.7} color="#bcd7ff" />}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Stage 2 default: premium industrial rig — readable ambient, key with
|
{/* Light Ozon studio: cool ambient, neutral key, soft shadows */}
|
||||||
PCF shadows, soft fill, cool rim (Stage 0 proven values, brightened
|
<ambientLight intensity={0.72} />
|
||||||
in 2B so brackets/rollers read as volumes from every angle) */}
|
<hemisphereLight args={[INDUSTRIAL_PALETTE.lightFill, INDUSTRIAL_PALETTE.gridCell, 0.85]} />
|
||||||
<ambientLight intensity={0.46} />
|
|
||||||
<hemisphereLight args={['#314860', '#0a1018', 0.9]} />
|
|
||||||
<directionalLight
|
<directionalLight
|
||||||
position={[6, 9, 4]}
|
position={[6, 9, 4]}
|
||||||
intensity={2.7}
|
intensity={1.55}
|
||||||
|
color={INDUSTRIAL_PALETTE.lightKey}
|
||||||
castShadow={shadowsEnabled}
|
castShadow={shadowsEnabled}
|
||||||
shadow-mapSize-width={1024}
|
shadow-mapSize-width={1024}
|
||||||
shadow-mapSize-height={1024}
|
shadow-mapSize-height={1024}
|
||||||
@@ -1221,46 +1239,44 @@ function ContinuousScene({
|
|||||||
shadow-camera-bottom={-7}
|
shadow-camera-bottom={-7}
|
||||||
shadow-camera-near={1}
|
shadow-camera-near={1}
|
||||||
shadow-camera-far={25}
|
shadow-camera-far={25}
|
||||||
shadow-bias={-0.00035}
|
shadow-bias={-0.0003}
|
||||||
shadow-normalBias={0.025}
|
shadow-normalBias={0.03}
|
||||||
/>
|
/>
|
||||||
<directionalLight position={[-5, 6, -3]} intensity={0.7} />
|
<directionalLight position={[-5, 6, -3]} intensity={0.55} color={INDUSTRIAL_PALETTE.lightFill} />
|
||||||
<directionalLight position={[2, 5, -8]} intensity={0.85} color="#bcd7ff" />
|
<directionalLight position={[2, 5, -8]} intensity={0.45} color={INDUSTRIAL_PALETTE.sensorActive} />
|
||||||
{/* low front fill so the +Z face (camera side) never goes black */}
|
<directionalLight position={[1, 3, 8]} intensity={0.4} color={INDUSTRIAL_PALETTE.lightKey} />
|
||||||
<directionalLight position={[1, 3, 8]} intensity={0.55} color="#cfdcf2" />
|
|
||||||
{/* Procedural studio environment (no external HDRI — offline-safe) */}
|
|
||||||
{shadowsEnabled && (
|
{shadowsEnabled && (
|
||||||
<Environment resolution={64} frames={1}>
|
<Environment resolution={64} frames={1}>
|
||||||
<Lightformer intensity={1.6} position={[0, 5, 0]} rotation-x={Math.PI / 2} scale={[8, 8, 1]} color="#dfe9ff" />
|
<Lightformer intensity={1.8} position={[0, 5, 0]} rotation-x={Math.PI / 2} scale={[8, 8, 1]} color={INDUSTRIAL_PALETTE.lightKey} />
|
||||||
<Lightformer intensity={0.7} position={[-5, 2, -4]} rotation-y={Math.PI / 3} scale={[4, 2, 1]} color="#b8c8e8" />
|
<Lightformer intensity={0.6} position={[-5, 2, -4]} rotation-y={Math.PI / 3} scale={[4, 2, 1]} color={INDUSTRIAL_PALETTE.lightFill} />
|
||||||
<Lightformer intensity={0.5} position={[5, 1.5, 3]} rotation-y={-Math.PI / 4} scale={[3, 1.5, 1]} color="#ffe9c8" />
|
<Lightformer intensity={0.35} position={[5, 1.5, 3]} rotation-y={-Math.PI / 4} scale={[3, 1.5, 1]} color={INDUSTRIAL_PALETTE.lightKey} />
|
||||||
</Environment>
|
</Environment>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Grid — subdued so equipment remains the visual subject */}
|
{/* Grid — subtle Ozon blue-gray */}
|
||||||
<Grid
|
<Grid
|
||||||
args={[16, 12]}
|
args={[16, 12]}
|
||||||
cellSize={0.5}
|
cellSize={0.5}
|
||||||
cellThickness={0.22}
|
cellThickness={0.28}
|
||||||
cellColor={darkBg ? '#1a2533' : '#1e2a38'}
|
cellColor={darkBg ? INDUSTRIAL_PALETTE.metalDark : INDUSTRIAL_PALETTE.gridCell}
|
||||||
sectionSize={2}
|
sectionSize={2}
|
||||||
sectionThickness={0.45}
|
sectionThickness={0.4}
|
||||||
sectionColor={darkBg ? '#243344' : '#2a3a4c'}
|
sectionColor={darkBg ? INDUSTRIAL_PALETTE.frame : INDUSTRIAL_PALETTE.sensorActive}
|
||||||
fadeDistance={10}
|
fadeDistance={12}
|
||||||
infiniteGrid={false}
|
infiniteGrid={false}
|
||||||
position={[0, 0.001, 0]}
|
position={[0, 0.001, 0]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Floor — dark polished concrete with soft reflections */}
|
{/* Floor — light cool gray */}
|
||||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]} receiveShadow={shadowsEnabled}>
|
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]} receiveShadow={shadowsEnabled}>
|
||||||
<planeGeometry args={[16, 12]} />
|
<planeGeometry args={[16, 12]} />
|
||||||
<meshStandardMaterial
|
<meshStandardMaterial
|
||||||
color={darkBg ? '#121a26' : '#151d2a'}
|
color={darkBg ? '#121a26' : INDUSTRIAL_PALETTE.floor}
|
||||||
roughness={darkBg ? 0.64 : 0.8}
|
roughness={darkBg ? 0.64 : 0.88}
|
||||||
metalness={darkBg ? 0.2 : 0.1}
|
metalness={darkBg ? 0.2 : 0.04}
|
||||||
envMapIntensity={0.5}
|
envMapIntensity={0.35}
|
||||||
/>
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,55 @@
|
|||||||
/**
|
/**
|
||||||
* Shared industrial visual tokens for the continuous digital twin.
|
* Shared visual tokens for the continuous digital twin.
|
||||||
* Keeps both modes looking like projections of one system.
|
* Ozon light presentation environment (runtime materials / lights only).
|
||||||
|
* CSS mirrors these values in `src/styles.css` (:root).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/** Canonical Ozon brand + semantic tokens (source of truth for tests / 3D). */
|
||||||
|
export const OZON_TOKENS = {
|
||||||
|
blue: '#005BFF',
|
||||||
|
magenta: '#F1117E',
|
||||||
|
darkSpace: '#001A34',
|
||||||
|
morning: '#00A2FF',
|
||||||
|
green: '#00BE6C',
|
||||||
|
orange: '#FFA800',
|
||||||
|
white: '#FFFFFF',
|
||||||
|
page: '#F5F7FA',
|
||||||
|
surface: '#FFFFFF',
|
||||||
|
surfaceSoft: '#EEF4FF',
|
||||||
|
border: '#DCE6F5',
|
||||||
|
muted: '#5F6F82',
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const INDUSTRIAL_PALETTE = {
|
export const INDUSTRIAL_PALETTE = {
|
||||||
background: '#e8eef4',
|
/** Scene clear / fog — soft Ozon morning surface */
|
||||||
backgroundDark: '#0b1220',
|
background: OZON_TOKENS.surfaceSoft,
|
||||||
floor: '#d5dde8',
|
backgroundDark: OZON_TOKENS.darkSpace,
|
||||||
gridCell: '#c5d0de',
|
floor: '#E2EAF5',
|
||||||
gridSection: '#9aabbf',
|
gridCell: '#C5D4EA',
|
||||||
|
gridSection: OZON_TOKENS.blue,
|
||||||
belt: '#2f3a48',
|
belt: '#2f3a48',
|
||||||
beltStripe: '#f1c40f',
|
beltStripe: OZON_TOKENS.orange,
|
||||||
metal: '#7b8796',
|
metal: '#7b8796',
|
||||||
metalDark: '#4a5563',
|
metalDark: '#4a5563',
|
||||||
frame: '#5b6b7c',
|
frame: '#5b6b7c',
|
||||||
plastic: '#94a3b8',
|
plastic: '#94a3b8',
|
||||||
rubber: '#1f2937',
|
rubber: '#1f2937',
|
||||||
cardboard: '#b68b58',
|
cardboard: '#b68b58',
|
||||||
sensorAccent: '#3b82f6',
|
/** Ozon blue — primary technical markers */
|
||||||
routeB: '#16a34a',
|
sensorAccent: OZON_TOKENS.blue,
|
||||||
routeC: '#ea580c',
|
sensorActive: OZON_TOKENS.morning,
|
||||||
routeD: '#7c3aed',
|
/** Success / B */
|
||||||
fault: '#ef4444',
|
routeB: OZON_TOKENS.green,
|
||||||
warning: '#f59e0b',
|
/** Attention / C */
|
||||||
lightKey: '#f8fafc',
|
routeC: OZON_TOKENS.orange,
|
||||||
lightFill: '#d0dae8',
|
/** Active selection / D accent (magenta used selectively) */
|
||||||
|
routeD: OZON_TOKENS.magenta,
|
||||||
|
fault: OZON_TOKENS.magenta,
|
||||||
|
warning: OZON_TOKENS.orange,
|
||||||
|
success: OZON_TOKENS.green,
|
||||||
|
lightKey: OZON_TOKENS.white,
|
||||||
|
lightFill: OZON_TOKENS.surfaceSoft,
|
||||||
|
text: OZON_TOKENS.darkSpace,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const INDUSTRIAL_MATERIALS = {
|
export const INDUSTRIAL_MATERIALS = {
|
||||||
|
|||||||
96
src/domain/productVisualOzon.test.ts
Normal file
96
src/domain/productVisualOzon.test.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import {
|
||||||
|
SETTLE_DURATION_SEC,
|
||||||
|
SETTLE_LINEAR_SPEED_MPS,
|
||||||
|
SETTLE_ANGULAR_SPEED_RAD_S,
|
||||||
|
DOCUMENTED_CONTACT_PLANE_S,
|
||||||
|
DOCUMENTED_CLEAR_PLANE_S,
|
||||||
|
} from './junctionContactPhysics';
|
||||||
|
import { BELT_SPEED_MPS } from './productPhysicsProfiles';
|
||||||
|
import { PHYSICS_TIMESTEP_SEC } from './physicsTimestep';
|
||||||
|
import {
|
||||||
|
DIVERTER_LEFT_SIGNED_DEG,
|
||||||
|
DIVERTER_RIGHT_SIGNED_DEG,
|
||||||
|
categoryToPhysicalRoute,
|
||||||
|
OPENING_SAFETY_MARGIN_SEC,
|
||||||
|
rotationDurationSec,
|
||||||
|
} from './pusherMotion';
|
||||||
|
import { INDUSTRIAL_PALETTE, OZON_TOKENS } from './industrialTheme';
|
||||||
|
import physicsSrc from '../components/ThreeD/PhysicalPlaybackItemPhysics.tsx?raw';
|
||||||
|
import visualSrc from '../components/ThreeD/PhysicalPlaybackItem.tsx?raw';
|
||||||
|
import sceneSrc from '../components/ThreeD/SorterDigitalTwinContinuous.tsx?raw';
|
||||||
|
import mainSrc from '../main.tsx?raw';
|
||||||
|
|
||||||
|
describe('product visual / rigid-body sync contracts', () => {
|
||||||
|
it('keeps a single RigidBody visual hierarchy with mount-only pose + memo', () => {
|
||||||
|
expect(physicsSrc).toMatch(/<RigidBody[\s\S]*ProductVisualGroup[\s\S]*ItemVisualContent[\s\S]*<\/RigidBody>/);
|
||||||
|
expect(physicsSrc).toMatch(/name="ProductVisualGroup"/);
|
||||||
|
// Initial pose is allowed; continuous type control is not (fights Dynamic).
|
||||||
|
expect(physicsSrc).toMatch(/position=\{spawnPose\.position\}/);
|
||||||
|
expect(physicsSrc).not.toMatch(/type="kinematicPosition"/);
|
||||||
|
expect(physicsSrc).toMatch(/setBodyType\(RigidBodyType\.Dynamic/);
|
||||||
|
expect(physicsSrc).toMatch(/Block playback-tick re-renders/);
|
||||||
|
expect(physicsSrc).toMatch(/castShadow=\{castShadow && spawned\}/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('visual model uses only local pivot offset (no world position writer)', () => {
|
||||||
|
expect(visualSrc).toMatch(/pivotOffsetY/);
|
||||||
|
expect(visualSrc).toMatch(/<group position=\{\[0, pivotOffsetY, 0\]\}>/);
|
||||||
|
expect(visualSrc).not.toMatch(/position=\{pose\.position\}/);
|
||||||
|
expect(visualSrc).not.toMatch(/position=\{currentWorld/);
|
||||||
|
expect(visualSrc).not.toMatch(/mesh\.position\.copy/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('one physics instance path remains (no duplicate PhysicalPlaybackItem mount)', () => {
|
||||||
|
const physicsMounts = sceneSrc.match(/PhysicalPlaybackItemPhysics/g) ?? [];
|
||||||
|
const kinematicMounts = sceneSrc.match(/<PhysicalPlaybackItem[\s>]/g) ?? [];
|
||||||
|
expect(physicsMounts.length).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(kinematicMounts.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('falling / physics freeze regression', () => {
|
||||||
|
it('receiver settle thresholds unchanged', () => {
|
||||||
|
expect(SETTLE_LINEAR_SPEED_MPS).toBe(0.20);
|
||||||
|
expect(SETTLE_ANGULAR_SPEED_RAD_S).toBe(1.0);
|
||||||
|
expect(SETTLE_DURATION_SEC).toBe(0.30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('B/C/D routing, belt, timestep, diverter timing unchanged', () => {
|
||||||
|
expect(categoryToPhysicalRoute('B')).toBe('STRAIGHT');
|
||||||
|
expect(categoryToPhysicalRoute('C')).toBe('PHYSICAL_LEFT');
|
||||||
|
expect(categoryToPhysicalRoute('D')).toBe('PHYSICAL_RIGHT');
|
||||||
|
expect(DIVERTER_LEFT_SIGNED_DEG).toBe(-45);
|
||||||
|
expect(DIVERTER_RIGHT_SIGNED_DEG).toBe(45);
|
||||||
|
expect(rotationDurationSec()).toBeCloseTo(0.5, 6);
|
||||||
|
expect(OPENING_SAFETY_MARGIN_SEC).toBeCloseTo(0.15, 6);
|
||||||
|
expect(DOCUMENTED_CONTACT_PLANE_S).toBe(1.0538);
|
||||||
|
expect(DOCUMENTED_CLEAR_PLANE_S).toBe(1.6);
|
||||||
|
expect(BELT_SPEED_MPS).toBe(1.0);
|
||||||
|
expect(PHYSICS_TIMESTEP_SEC).toBeCloseTo(1 / 120, 12);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Ozon light theme tokens', () => {
|
||||||
|
it('defines centralized Ozon tokens; CSS stylesheet is wired', () => {
|
||||||
|
expect(OZON_TOKENS.blue).toBe('#005BFF');
|
||||||
|
expect(OZON_TOKENS.magenta).toBe('#F1117E');
|
||||||
|
expect(OZON_TOKENS.darkSpace).toBe('#001A34');
|
||||||
|
expect(OZON_TOKENS.morning).toBe('#00A2FF');
|
||||||
|
expect(OZON_TOKENS.green).toBe('#00BE6C');
|
||||||
|
expect(OZON_TOKENS.orange).toBe('#FFA800');
|
||||||
|
expect(OZON_TOKENS.page).toBe('#F5F7FA');
|
||||||
|
expect(OZON_TOKENS.surfaceSoft).toBe('#EEF4FF');
|
||||||
|
// Magenta is selective accent — never the page background token.
|
||||||
|
expect(OZON_TOKENS.page).not.toBe(OZON_TOKENS.magenta);
|
||||||
|
expect(mainSrc).toMatch(/styles\.css/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scene default is light Ozon environment', () => {
|
||||||
|
expect(INDUSTRIAL_PALETTE.background.toUpperCase()).toBe('#EEF4FF');
|
||||||
|
expect(INDUSTRIAL_PALETTE.sensorAccent.toUpperCase()).toBe('#005BFF');
|
||||||
|
expect(INDUSTRIAL_PALETTE.routeB.toUpperCase()).toBe('#00BE6C');
|
||||||
|
expect(sceneSrc).toMatch(/const darkBg = proto \? stage0!\.darkBackground : false/);
|
||||||
|
expect(sceneSrc).toMatch(/INDUSTRIAL_PALETTE\.background/);
|
||||||
|
});
|
||||||
|
});
|
||||||
207
src/styles.css
207
src/styles.css
@@ -1,25 +1,54 @@
|
|||||||
/* =========================================================
|
/* =========================================================
|
||||||
OZON Tech Sorter — Product Demo UI
|
OZON Tech Sorter — Product Demo UI (light Ozon palette)
|
||||||
========================================================= */
|
========================================================= */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
color-scheme: dark;
|
color-scheme: light;
|
||||||
--bg: #050910;
|
|
||||||
--panel: rgba(10, 19, 31, 0.92);
|
/* Brand tokens */
|
||||||
--panel-strong: #0c1725;
|
--ozon-blue: #005BFF;
|
||||||
--line: #1e3a54;
|
--ozon-magenta: #F1117E;
|
||||||
--muted: #91a4b8;
|
--ozon-dark-space: #001A34;
|
||||||
--text: #e5f2ff;
|
--ozon-morning: #00A2FF;
|
||||||
--cyan: #38bdf8;
|
--ozon-green: #00BE6C;
|
||||||
--cyan-hover: #7dd3fc;
|
--ozon-orange: #FFA800;
|
||||||
--green: #4ade80;
|
--ozon-white: #FFFFFF;
|
||||||
--orange: #f59e0b;
|
--ozon-page: #F5F7FA;
|
||||||
--purple: #c084fc;
|
--ozon-surface: #FFFFFF;
|
||||||
--red: #fb3d4e;
|
--ozon-surface-soft: #EEF4FF;
|
||||||
|
--ozon-border: #DCE6F5;
|
||||||
|
--ozon-text: #001A34;
|
||||||
|
--ozon-muted: #5F6F82;
|
||||||
|
|
||||||
|
/* Semantic */
|
||||||
|
--brand-primary: var(--ozon-blue);
|
||||||
|
--brand-secondary: var(--ozon-magenta);
|
||||||
|
--brand-light: var(--ozon-morning);
|
||||||
|
--status-success: var(--ozon-green);
|
||||||
|
--status-warning: var(--ozon-orange);
|
||||||
|
--text-primary: var(--ozon-dark-space);
|
||||||
|
--page-background: var(--ozon-page);
|
||||||
|
--panel-background: var(--ozon-surface);
|
||||||
|
--panel-border: var(--ozon-border);
|
||||||
|
--focus-ring: var(--ozon-blue);
|
||||||
|
|
||||||
|
/* Compat aliases used across the stylesheet */
|
||||||
|
--bg: var(--page-background);
|
||||||
|
--panel: var(--panel-background);
|
||||||
|
--panel-strong: var(--ozon-surface-soft);
|
||||||
|
--line: var(--panel-border);
|
||||||
|
--muted: var(--ozon-muted);
|
||||||
|
--text: var(--text-primary);
|
||||||
|
--cyan: var(--brand-primary);
|
||||||
|
--cyan-hover: var(--brand-light);
|
||||||
|
--green: var(--status-success);
|
||||||
|
--orange: var(--status-warning);
|
||||||
|
--purple: var(--brand-secondary);
|
||||||
|
--red: var(--brand-secondary);
|
||||||
--section-max: 1200px;
|
--section-max: 1200px;
|
||||||
--radius: 16px;
|
--radius: 14px;
|
||||||
--tap: 44px;
|
--tap: 44px;
|
||||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
font-family: "Manrope", "Segoe UI", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
*,
|
*,
|
||||||
@@ -45,9 +74,9 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 18% 8%, rgba(56, 189, 248, 0.14), transparent 28%),
|
radial-gradient(circle at 12% 0%, rgba(0, 162, 255, 0.10), transparent 32%),
|
||||||
radial-gradient(circle at 88% 18%, rgba(192, 132, 252, 0.1), transparent 24%),
|
radial-gradient(circle at 88% 8%, rgba(0, 91, 255, 0.06), transparent 28%),
|
||||||
linear-gradient(160deg, #03060b 0%, #07111d 52%, #050910 100%);
|
linear-gradient(180deg, var(--ozon-surface-soft) 0%, var(--page-background) 42%, var(--page-background) 100%);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
@@ -77,25 +106,26 @@ a {
|
|||||||
|
|
||||||
button {
|
button {
|
||||||
min-height: var(--tap);
|
min-height: var(--tap);
|
||||||
border: 1px solid #2a4a66;
|
border: 1px solid var(--panel-border);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: #0e1b2b;
|
background: var(--panel-background);
|
||||||
color: var(--text);
|
color: var(--text-primary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
transition: transform 160ms ease, border-color 160ms ease, background 160ms ease;
|
box-shadow: 0 1px 2px rgba(0, 26, 52, 0.06);
|
||||||
|
transition: transform 160ms ease, border-color 160ms ease, background 160ms ease, box-shadow 160ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
border-color: var(--cyan);
|
border-color: var(--brand-primary);
|
||||||
background: #11273b;
|
background: var(--ozon-surface-soft);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
button:focus-visible,
|
button:focus-visible,
|
||||||
.nav-link:focus-visible,
|
.nav-link:focus-visible,
|
||||||
.criteria-link:focus-visible {
|
.criteria-link:focus-visible {
|
||||||
outline: 2px solid var(--cyan);
|
outline: 2px solid var(--focus-ring);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,19 +151,20 @@ button:disabled {
|
|||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
border: none;
|
border: none;
|
||||||
background: linear-gradient(135deg, #38bdf8, #0ea5e9);
|
background: var(--brand-primary);
|
||||||
color: #041018;
|
color: var(--ozon-white);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 91, 255, 0.28);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
background: linear-gradient(135deg, #7dd3fc, #38bdf8);
|
background: var(--brand-light);
|
||||||
color: #041018;
|
color: var(--ozon-white);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background: #0e1b2b;
|
background: var(--panel-background);
|
||||||
color: var(--text);
|
color: var(--text-primary);
|
||||||
border: 1px solid #2a4a66;
|
border: 1px solid var(--panel-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-B { color: var(--green); }
|
.category-B { color: var(--green); }
|
||||||
@@ -3686,7 +3717,7 @@ button:disabled {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #070d16;
|
background: var(--page-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-page--product .product-topbar,
|
.main-page--product .product-topbar,
|
||||||
@@ -3701,8 +3732,8 @@ button:disabled {
|
|||||||
height: 68px;
|
height: 68px;
|
||||||
max-height: 72px;
|
max-height: 72px;
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
border-bottom: 1px solid rgba(56, 189, 248, 0.18);
|
border-bottom: 1px solid var(--panel-border);
|
||||||
background: rgba(6, 12, 20, 0.88);
|
background: var(--ozon-white);
|
||||||
backdrop-filter: blur(14px);
|
backdrop-filter: blur(14px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3720,9 +3751,9 @@ button:disabled {
|
|||||||
width: 34px;
|
width: 34px;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
border-radius: 9px;
|
border-radius: 9px;
|
||||||
border: 1px solid rgba(56, 189, 248, 0.4);
|
border: 1px solid var(--panel-border);
|
||||||
background: rgba(56, 189, 248, 0.12);
|
background: var(--ozon-surface-soft);
|
||||||
color: #7dd3fc;
|
color: var(--brand-primary);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
@@ -3732,7 +3763,7 @@ button:disabled {
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
color: #e8f2ff;
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-brand-sub {
|
.product-brand-sub {
|
||||||
@@ -3829,7 +3860,7 @@ button:disabled {
|
|||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
background: #070d16;
|
background: var(--page-background);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3840,8 +3871,8 @@ button:disabled {
|
|||||||
height: 78px;
|
height: 78px;
|
||||||
max-height: 84px;
|
max-height: 84px;
|
||||||
padding: 0 24px 10px;
|
padding: 0 24px 10px;
|
||||||
border-top: 1px solid rgba(56, 189, 248, 0.18);
|
border-top: 1px solid var(--panel-border);
|
||||||
background: rgba(6, 12, 20, 0.9);
|
background: var(--ozon-white);
|
||||||
backdrop-filter: blur(14px);
|
backdrop-filter: blur(14px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3895,7 +3926,9 @@ button:disabled {
|
|||||||
position: static;
|
position: static;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: rgba(12, 22, 34, 0.9);
|
background: var(--ozon-surface-soft);
|
||||||
|
border: 1px solid var(--panel-border);
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-sim-pill {
|
.product-sim-pill {
|
||||||
@@ -3990,3 +4023,89 @@ button:disabled {
|
|||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
Ozon light chrome overrides (shell / HUD / nav / controls)
|
||||||
|
========================================================= */
|
||||||
|
|
||||||
|
.main-hud,
|
||||||
|
.app-nav-overlay,
|
||||||
|
.app-nav-bar,
|
||||||
|
.main-progress {
|
||||||
|
background: rgba(255, 255, 255, 0.92) !important;
|
||||||
|
border-color: var(--panel-border) !important;
|
||||||
|
color: var(--text-primary);
|
||||||
|
box-shadow: 0 4px 18px rgba(0, 26, 52, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-button,
|
||||||
|
.main-page--product .play-button {
|
||||||
|
background: var(--brand-primary) !important;
|
||||||
|
color: var(--ozon-white) !important;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 91, 255, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-button:hover {
|
||||||
|
background: var(--brand-light) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-nav-link.active {
|
||||||
|
color: var(--ozon-white) !important;
|
||||||
|
background: var(--brand-primary) !important;
|
||||||
|
border-color: var(--brand-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-nav-link:hover {
|
||||||
|
color: var(--brand-primary);
|
||||||
|
border-color: var(--panel-border);
|
||||||
|
background: var(--ozon-surface-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-topbar,
|
||||||
|
.docs-product-topbar,
|
||||||
|
.main-page--product .product-topbar,
|
||||||
|
.docs-page .product-topbar {
|
||||||
|
background: var(--ozon-white) !important;
|
||||||
|
border-bottom: 1px solid var(--panel-border) !important;
|
||||||
|
color: var(--text-primary) !important;
|
||||||
|
box-shadow: 0 1px 0 rgba(0, 26, 52, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-brand-title,
|
||||||
|
.product-tele-value {
|
||||||
|
color: var(--text-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-brand-sub,
|
||||||
|
.product-tele-label {
|
||||||
|
color: var(--ozon-muted) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-page--product .app-nav-solid,
|
||||||
|
.docs-page .product-topbar .app-nav-solid {
|
||||||
|
background: var(--ozon-surface-soft) !important;
|
||||||
|
border-color: var(--panel-border) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-webgl-note,
|
||||||
|
.docs-page {
|
||||||
|
background: var(--page-background) !important;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-tele-value.category-B { color: var(--status-success) !important; }
|
||||||
|
.product-tele-value.category-C { color: var(--status-warning) !important; }
|
||||||
|
.product-tele-value.category-D { color: var(--brand-secondary) !important; }
|
||||||
|
|
||||||
|
.progress-dot {
|
||||||
|
background: var(--ozon-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-page--product .product-dock,
|
||||||
|
.main-page--product .main-controls .auto-camera-toggle,
|
||||||
|
.main-page--product .presentation-toggle,
|
||||||
|
.stop-button {
|
||||||
|
background: var(--ozon-white) !important;
|
||||||
|
border-color: var(--panel-border) !important;
|
||||||
|
color: var(--text-primary) !important;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user