Open Source · CC BY-SA 4.0

Код, который думает
в миллиметрах

TCSL — типобезопасный DSL, где 732 mm + 45 deg — ошибка, а не 777. LLM генерирует → парсер проверяет за 50 мс → FreeCAD строит

~50
строк на тумбу
5
размерных типов
32
кода ошибок
<50ms
валидация
Length × Length = Area — статически, до рендераЧисло 16 — 14 раз в Python, 1 раз в TCSLLL(2) грамматика — LLM генерирует без ошибокexport as — калькулятор считаетLength × Length = Area — статически, до рендераЧисло 16 — 14 раз в Python, 1 раз в TCSLLL(2) грамматика — LLM генерирует без ошибокexport as — калькулятор считает
Из манифеста TCSL

Пять принципов
из десяти

Каждое решение в грамматике восходит к этим принципам. Полный манифест — 10 принципов, 5 обязательств, границы и эволюция

Проблема

Python + FreeCAD:
~280 строк боли

Тумба 732×550×900 мм, 4 ящика. Прокрутите — это реальный объём кода

Python / FreeCAD— реальный рабочий скрипт
~280 строк
import FreeCAD
import Part

doc = FreeCAD.newDocument("Cabinet_4Drawers")

# ═══════════════════════════════════════════
# ПАРАМЕТРЫ — все числа без единиц!
# Если кто-то передаст дюймы — удачи.
# ═══════════════════════════════════════════
total_width  = 732    # мм? дюймы? кто знает
total_depth  = 550
side_height  = 900
board        = 16
back_board   = 3
guide_w      = 13
niche_h      = 150
num_drawers  = 4
gap          = 3
face_gap     = 2
fasad_board  = 19

# ═══════════════════════════════════════════
# ПРОИЗВОДНЫЕ — вручную, формулы дублируются
# ═══════════════════════════════════════════
inner_width      = total_width - 2 * board
inner_depth      = total_depth - back_board
drawer_width     = inner_width - 2 * guide_w
usable_height    = side_height - 2 * board - niche_h
drawer_step      = usable_height / num_drawers
drawer_height    = drawer_step - gap
face_width       = drawer_width - 2 * face_gap
face_height      = drawer_height - face_gap
drawer_box_depth = inner_depth - 30
drawer_box_h     = drawer_height - 20
drawer_box_side  = 12
drawer_box_back  = 12
drawer_box_bot   = 4

z_bot       = 0
z_bot_top   = z_bot + board
z_niche_top = z_bot_top + niche_h
z_d1        = z_niche_top + gap
z_d2        = z_d1 + drawer_step
z_d3        = z_d2 + drawer_step
z_d4        = z_d3 + drawer_step
z_top_bot   = side_height - board

# ═══════════════════════════════════════════
# КОРПУС — 5 деталей
# ═══════════════════════════════════════════

# --- Левая боковина ---
left_side = doc.addObject("Part::Box", "LeftSide")
left_side.Length = board
left_side.Width  = total_depth
left_side.Height = side_height
left_side.Placement = FreeCAD.Placement(
    FreeCAD.Vector(0, 0, 0),
    FreeCAD.Rotation(0, 0, 0, 1))
left_side.Label = "Bok_LDSP16mm_1"

# --- Правая боковина ---
right_side = doc.addObject("Part::Box", "RightSide")
right_side.Length = board
right_side.Width  = total_depth
right_side.Height = side_height
right_side.Placement = FreeCAD.Placement(
    FreeCAD.Vector(total_width - board, 0, 0),
    FreeCAD.Rotation(0, 0, 0, 1))
right_side.Label = "Bok_LDSP16mm_2"

# --- Дно ---
bottom = doc.addObject("Part::Box", "Bottom")
bottom.Length = inner_width
bottom.Width  = total_depth
bottom.Height = board
bottom.Placement = FreeCAD.Placement(
    FreeCAD.Vector(board, 0, z_bot),
    FreeCAD.Rotation(0, 0, 0, 1))
bottom.Label = "Dno_LDSP16mm_1"

# --- Крышка ---
top_panel = doc.addObject("Part::Box", "TopPanel")
top_panel.Length = inner_width
top_panel.Width  = total_depth
top_panel.Height = board
top_panel.Placement = FreeCAD.Placement(
    FreeCAD.Vector(board, 0, z_top_bot),
    FreeCAD.Rotation(0, 0, 0, 1))
top_panel.Label = "Krysha_LDSP16mm_1"

# --- Задняя стенка ---
back_panel = doc.addObject("Part::Box", "BackPanel")
back_panel.Length = inner_width
back_panel.Width  = back_board
back_panel.Height = side_height - 2 * board
back_panel.Placement = FreeCAD.Placement(
    FreeCAD.Vector(board, total_depth - back_board, z_bot_top),
    FreeCAD.Rotation(0, 0, 0, 1))
back_panel.Label = "Zadstena_DVP3mm_1"

# ═══════════════════════════════════════════
# ЯЩИКИ 1–4 — копипаст ×4
# (сокращено для читаемости)
# ═══════════════════════════════════════════
# ...ещё ~160 строк копипаста ящиков...

doc.recompute()
doc.saveAs("/tmp/cabinet_4drawers.FCStd")
~280 строк — прокрутите ↕
Решение

Тот же шкаф.
50 строк TCSL

Та же тумба, те же 9 деталей. Окно в 3× меньше — и код всё равно помещается

TCSL v1.5— полная тумба, 9 деталей
50 строк
# ═══════════════════════════════════
# ЗОНА 1: INPUT — базовые параметры
# ═══════════════════════════════════
input total_width   = 732 mm
input total_depth   = 550 mm
input side_height   = 900 mm
input board         = 16 mm
input back_board    = 3 mm
input guide_w       = 13 mm
input niche_h       = 150 mm
input num_drawers   = 4
input gap           = 3 mm
input face_gap      = 2 mm
input fasad_board   = 19 mm

# ═══════════════════════════════════
# ЗОНА 2: LET — производные
# ═══════════════════════════════════
let inner_width  = total_width - 2 * board
let drawer_width = inner_width - 2 * guide_w
let usable_h     = side_height - 2 * board - niche_h
let drawer_step  = usable_h / num_drawers
let face_width   = drawer_width - 2 * face_gap
let face_height  = drawer_step - gap - face_gap
let z_d1 = board + niche_h + gap
let z_d2 = z_d1 + drawer_step
let z_d3 = z_d2 + drawer_step
let z_d4 = z_d3 + drawer_step

# ═══════════════════════════════════
# ЗОНА 3: GEOMETRY
# ═══════════════════════════════════
left_side  = box(board, total_depth, side_height)
right_side = box(board, total_depth, side_height) |> translate(total_width - board, 0 mm, 0 mm)
bottom     = box(inner_width, total_depth, board) |> translate(board, 0 mm, 0 mm)
top_panel  = box(inner_width, total_depth, board) |> translate(board, 0 mm, side_height - board)
back       = box(inner_width, back_board, usable_h) |> translate(board, total_depth - back_board, board)
front_1    = box(face_width, fasad_board, face_height) |> translate(board+guide_w+face_gap, 0mm-fasad_board, z_d1+face_gap)
front_2    = box(face_width, fasad_board, face_height) |> translate(board+guide_w+face_gap, 0mm-fasad_board, z_d2+face_gap)
front_3    = box(face_width, fasad_board, face_height) |> translate(board+guide_w+face_gap, 0mm-fasad_board, z_d3+face_gap)
front_4    = box(face_width, fasad_board, face_height) |> translate(board+guide_w+face_gap, 0mm-fasad_board, z_d4+face_gap)

# ═══════════════════════════════════
# ЗОНА 4: EXPORT
# ═══════════════════════════════════
export left_side   as Bok_LDSP16mm_1
export right_side  as Bok_LDSP16mm_2
export bottom      as Dno_LDSP16mm_1
export top_panel   as Krysha_LDSP16mm_1
export back        as Zadstena_DVP3mm_1
export front_1     as Fasad_MDF19mm_1
export front_2     as Fasad_MDF19mm_2
export front_3     as Fasad_MDF19mm_3
export front_4     as Fasad_MDF19mm_4
50 строк — прокрутите ↕
280
строк Python
4× копипаст · 0 единиц · формулы дублируются
50
строк TCSL
1× input · автопересчёт · единицы · export as
×5.6 меньше кода При тех же 9 деталях и том же результате. Почему именно так — принцип №7
Архитектура

Четыре зоны — одна модель

Что меняет заказчик → что вычисляет инженер → что строит станок → что считает калькулятор. Принцип №2 манифеста

01

INPUT

Заказчик

Литералы с единицами. Переопределяемы из GUI или API. Контракт с внешним миром

input width = 732 mm
input board = 16 mm
02

LET

Инженер

Формулы. Автопересчёт при изменении input. Алгебра L⁰–L³ проверяется статически

let inner = width - 2 * board
let area = inner * depth
03

GEOMETRY

Станок

23 функции. Pipeline |> — цепочка трансформаций

side = box(board, depth, h)
  |> translate(0mm,0mm,0mm)
04

EXPORT

Калькулятор

Имена для FreeCAD и себестоимости. Материал, толщина, номер — автоматически

export side as Bok_LDSP16mm_1

Почему нет if/for/функций? Это не ограничение — это архитектурное решение

Мебель — это математика
Математика заслуживает языка

Спецификация, EBNF-грамматика, VS Code, парсер Python. Open source

Вопросы: texttocad@ya.ru

CC BY-SA 4.0 · TCSL v1.5 · 2026-03-18