feat(org-chart): add core cards and fixed-layout integration
This commit is contained in:
1 parent
d18e3c3380
commit
61c6f8d1f1
161 files changed
+13044
-1
No files matched your search
@@ -0,0 +1,116 @@
|
||||
"""Extract business-management PNG colors into matching vector card templates.
|
||||
|
||||
Reads the reference only. No bitmap is embedded in the generated SVGs.
|
||||
Usage: python create-business-card-assets.py <reference PNG path>
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
reference = Path(sys.argv[1])
|
||||
image = Image.open(reference).convert('RGBA')
|
||||
pixels = np.asarray(image, dtype=float)
|
||||
width, height = image.size
|
||||
scale = np.array([width / 146, height / 68])
|
||||
start = np.array([124.527, 13.2139]) * scale
|
||||
end = np.array([91.4091, 70.3397]) * scale
|
||||
vector = end - start
|
||||
|
||||
# Probe only the flat outer bands, away from rounded corners and inner face.
|
||||
probes = ([(x, 2) for x in range(20, width - 20)]
|
||||
+ [(3, y) for y in range(20, height - 15)]
|
||||
+ [(width - 4, y) for y in range(20, height - 15)]
|
||||
+ [(x, height - 3) for x in range(20, width - 20)])
|
||||
coordinates = np.array(probes, dtype=float) + 0.5
|
||||
times = np.clip((coordinates - start) @ vector / (vector @ vector), 0, 1)
|
||||
values = np.array([pixels[y, x] for x, y in probes])
|
||||
basis = np.zeros((len(times), 3))
|
||||
first = times <= 0.72
|
||||
basis[first, 0] = 1 - times[first] / 0.72
|
||||
basis[first, 1] = times[first] / 0.72
|
||||
basis[~first, 1] = (1 - times[~first]) / 0.28
|
||||
basis[~first, 2] = (times[~first] - 0.72) / 0.28
|
||||
|
||||
# Test both interpolation representations rather than assuming raster alpha.
|
||||
fits = []
|
||||
for premultiplied in (False, True):
|
||||
samples = values.copy()
|
||||
if premultiplied:
|
||||
samples[:, :3] *= samples[:, 3:] / 255
|
||||
stops = np.linalg.lstsq(basis, samples, rcond=None)[0]
|
||||
predicted = basis @ stops
|
||||
if premultiplied:
|
||||
predicted[:, :3] /= np.maximum(predicted[:, 3:], 1) / 255
|
||||
stops[:, :3] /= np.maximum(stops[:, 3:], 1) / 255
|
||||
error = float(np.abs(predicted - values).mean())
|
||||
fits.append((error, premultiplied, np.clip(stops, 0, 255)))
|
||||
error, premultiplied, stops = min(fits, key=lambda item: item[0])
|
||||
|
||||
# Use plateau medians for the clamped endpoints, reducing raster edge noise.
|
||||
stops[0] = np.median(values[times == 0], axis=0)
|
||||
stops[2] = np.median(values[times == 1], axis=0)
|
||||
colors = ['#' + ''.join(f'{int(round(channel)):02X}' for channel in row[:3]) for row in stops]
|
||||
opacities = [round(float(row[3] / 255), 2) for row in stops]
|
||||
inner_top = pixels[9:35, 20:150].reshape(-1, 4)
|
||||
inner_bottom = pixels[height - 13:height - 6, 20:150].reshape(-1, 4)
|
||||
inner_colors = ['#' + ''.join(f'{int(round(channel)):02X}' for channel in row[:3])
|
||||
for row in [np.median(inner_top, axis=0), np.median(inner_bottom, axis=0)]]
|
||||
|
||||
# Recover the inner face's gradient direction from its unclamped RGB plane.
|
||||
# Colors alone are insufficient: retaining the template's gradient direction
|
||||
# would leave a visible tone difference despite matching all endpoint colors.
|
||||
inner_start_rgb = np.array([int(inner_colors[0][i:i + 2], 16) for i in (1, 3, 5)])
|
||||
inner_end_rgb = np.array([int(inner_colors[1][i:i + 2], 16) for i in (1, 3, 5)])
|
||||
delta = inner_start_rgb - inner_end_rgb
|
||||
inner_points, inner_times = [], []
|
||||
for y in range(12, height - 12, 2):
|
||||
for x in range(20, min(220, width // 2 + 16), 2):
|
||||
rgb = pixels[y, x, :3]
|
||||
t = float((inner_start_rgb - rgb) @ delta / (delta @ delta))
|
||||
if 0.06 < t < 0.94 and pixels[y, x, 3] == 255:
|
||||
inner_points.append([(x + 0.5) / scale[0], (y + 0.5) / scale[1], 1])
|
||||
inner_times.append(t)
|
||||
plane = np.linalg.lstsq(np.array(inner_points), np.array(inner_times), rcond=None)[0]
|
||||
gradient_vector = plane[:2] / (plane[:2] @ plane[:2])
|
||||
inner_start = np.array([78.0741, (-plane[2] - plane[0] * 78.0741) / plane[1]])
|
||||
inner_end = inner_start + gradient_vector
|
||||
|
||||
assets = root / 'packages/org-chart/src/assets/cards'
|
||||
for suffix in ('', '-sm'):
|
||||
template = (assets / f'경영기획팀{suffix}.svg').read_text(encoding='utf-8')
|
||||
# Retain the supplied department geometry and its size-specific gradients.
|
||||
for original, replacement in zip(['#DEE6F0', '#A8BBD1', '#577BA5'], colors):
|
||||
template = template.replace(f'stop-color="{original}"', f'stop-color="{replacement}"')
|
||||
template = template.replace(f'stop-color="{colors[2]}"', f'stop-color="{colors[2]}" stop-opacity="{opacities[2]}"')
|
||||
template = template.replace('stop-color="white"', f'stop-color="{inner_colors[0]}"')
|
||||
template = template.replace('stop-color="#E0E6F4"', f'stop-color="{inner_colors[1]}"')
|
||||
template = template.replace('paint0_linear_', 'business_management_outer_').replace('paint1_linear_', 'business_management_inner_')
|
||||
size_scale = np.array([1, 1]) if not suffix else np.array([108 / 146, 52 / 68])
|
||||
p1, p2 = inner_start * size_scale, inner_end * size_scale
|
||||
template = re.sub(r'(<linearGradient id="business_management_inner_[^"]+")[^>]+>',
|
||||
lambda match: f'{match[1]} x1="{p1[0]:.4f}" y1="{p1[1]:.4f}" x2="{p2[0]:.4f}" y2="{p2[1]:.4f}" gradientUnits="userSpaceOnUse">', template)
|
||||
template = template.replace('offset="0.97"', 'offset="1"')
|
||||
(assets / f'사업관리팀{suffix}.svg').write_text(template, encoding='utf-8')
|
||||
|
||||
output = root / 'test-results/org-chart-card-design'
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(reference, output / 'business-management-reference.png')
|
||||
evidence = {
|
||||
'reference': str(reference), 'sha256': hashlib.sha256(reference.read_bytes()).hexdigest(),
|
||||
'referenceSize': [width, height], 'outerStops': colors, 'outerOpacities': opacities,
|
||||
'innerStops': inner_colors, 'outerStopOffsets': [0, 0.72, 1],
|
||||
'innerGradientStart': inner_start.tolist(), 'innerGradientEnd': inner_end.tolist(),
|
||||
'innerSampleCount': len(inner_points),
|
||||
'sampleCount': len(probes), 'fitMeanAbsoluteRGBAError': error,
|
||||
'premultipliedFit': premultiplied,
|
||||
'method': 'Outer-band RGBA samples fitted to the supplied department gradient geometry; endpoint plateau medians and inner-face medians. Inner gradient direction recovered from unclamped RGB plane. Reference PNG is preserved; geometry reuses department SVG paths.',
|
||||
}
|
||||
(output / 'business-management-colors.json').write_text(json.dumps(evidence, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
print(json.dumps(evidence, ensure_ascii=False, indent=2))
|
||||
Reference in new issue
Block a user