kind=space (fixed oblique 3D: points, vectors, planes, box) + ~190 figured questions for grade 12 (geometry, discrete, calculus)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 01:04:08 +03:30
parent 998b68e4bc
commit 733796c25a
4 changed files with 6434 additions and 2 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -157,5 +157,7 @@ python tools/figures/batch.py merge batch.json # افزود
درس‌هایی که ذاتاً شکل ندارند (تعریف، گزاره، نقیض) را شکل نمی‌دهیم. بیضی، سهمی و هذلولی نوعِ جدا ندارند: درس‌هایی که ذاتاً شکل ندارند (تعریف، گزاره، نقیض) را شکل نمی‌دهیم. بیضی، سهمی و هذلولی نوعِ جدا ندارند:
با دو `function` (نیمه‌ی بالا و پایین) کشیده می‌شوند. معادله‌ی خط را به شکلِ `ax + by = c` بنویسید (`y = -x/3 + 1` با دو `function` (نیمه‌ی بالا و پایین) کشیده می‌شوند. معادله‌ی خط را به شکلِ `ax + by = c` بنویسید (`y = -x/3 + 1`
خوانده نمی‌شود)، و نسبتِ `xRange` به `yRange` را از حدود ۱٫۵ بیشتر نکنید، وگرنه خط‌ها نصفه کشیده می‌شوند. خوانده نمی‌شود و شکلِ بی‌خط درمی‌آید). بعد از `check`، شکل را حتماً رندر و با چشم ببینید؛ در رندرِ آزمایشیِ
بعد از `check`، شکل را حتماً رندر و با چشم ببینید. چند شکل در یک صفحه، به `renderToString` با `identifierPrefix` یکتا برای هر شکل نیاز است، وگرنه `useId` در همه‌ی شکل‌ها
یکی می‌شود و clipPathها روی هم می‌افتند و منحنی‌ها نصفه دیده می‌شوند (خطای ابزارِ آزمایش، نه اپ).
فضای سه‌بعدی: `{"kind":"space",…}` با نقطه، بردار، پاره‌خط، چندضلعی (صفحه)، جعبه و خط‌چین‌های تصویر؛ تصویرِ مایلِ ثابت.

View File

@@ -152,6 +152,14 @@ _SOLID = (
'مثلث، پنج‌ضلعی و شش‌ضلعی)' 'مثلث، پنج‌ضلعی و شش‌ضلعی)'
) )
_SPACE = (
'space (مختصات و بردار در فضای سه‌بعدی، تصویرِ مایل؛ برای R³، بردارِ سه‌مؤلفه‌ای، ضربِ داخلی و خارجی، خط و صفحه در فضا): '
'{"kind":"space","title":"…","axes":true,"points":{"A":[1,2,3],"B":[3,0,1]},"items":[…]}. محورها: x رو به بیننده، '
'y راست، z بالا. items: {"type":"point","point":"A","label":"A(1,2,3)"} · {"type":"vector","from":"O","to":"A","label":"a"} · '
'{"type":"segment","from":"A","to":"B","dashed":true} · {"type":"polygon","points":["A","B","C"],"label":"P"} (مثلث یا صفحه‌ی '
'متوازی‌الاضلاعی؛ نیمه‌شفاف) · {"type":"box","to":[3,2,4]} (مکعب‌مستطیلِ مختصاتی از مبدأ) · {"type":"drop","point":"A"} '
'(خط‌چین‌های تصویرِ نقطه روی صفحه‌های مختصات). مبدأ "O" خودکار است. رنگ فقط: blue red green orange purple teal.'
)
_W = lambda words: re.compile('|'.join(words)) # noqa: E731 _W = lambda words: re.compile('|'.join(words)) # noqa: E731
_GEOMETRY_WORDS = (r'مثلث', r'دایره(?!‌ای)', r'زاویه', r'ضلع', r'چندضلعی', r'مربع', r'مستطیل', r'لوزی', r'ذوزنقه', _GEOMETRY_WORDS = (r'مثلث', r'دایره(?!‌ای)', r'زاویه', r'ضلع', r'چندضلعی', r'مربع', r'مستطیل', r'لوزی', r'ذوزنقه',
r'متوازی', r'شعاع', r'قطر', r'وتر', r'محیط', r'مساحت', r'کمان', r'محاطی', r'مماس', r'موازی', r'متوازی', r'شعاع', r'قطر', r'وتر', r'محیط', r'مساحت', r'کمان', r'محاطی', r'مماس', r'موازی',
@@ -175,6 +183,7 @@ SECTIONS = [
(_W((r'احتمال', r'سکه', r'تاس', r'درختی')), _TREE), (_W((r'احتمال', r'سکه', r'تاس', r'درختی')), _TREE),
(_W((r'الگو', r'چوب\s*کبریت', r'دنباله', r'شکل[\u0650\s]*(?:بعدی|شماره|[0-9۰-۹])')), _PATTERN), (_W((r'الگو', r'چوب\s*کبریت', r'دنباله', r'شکل[\u0650\s]*(?:بعدی|شماره|[0-9۰-۹])')), _PATTERN),
(_W((r'مکعب', r'منشور', r'هرم', r'استوانه', r'مخروط', r'کره', r'حجم', r'نیم‌کره')), _SOLID), (_W((r'مکعب', r'منشور', r'هرم', r'استوانه', r'مخروط', r'کره', r'حجم', r'نیم‌کره')), _SOLID),
(_W((r'سه‌بعدی', r'R³', r'ضرب\s*(?:داخلی|خارجی)', r'مختصات[^.؟]{0,12}فضا', r'بردار[^.؟]{0,20}فضا')), _SPACE),
] ]
DIAGRAM_SPEC = '\n'.join([_HEAD] + [text for _, text in SECTIONS]) DIAGRAM_SPEC = '\n'.join([_HEAD] + [text for _, text in SECTIONS])
@@ -750,6 +759,68 @@ def _tree(node, depth=0) -> dict:
return out return out
_SPACE_COLORS = {'blue', 'red', 'green', 'orange', 'purple', 'teal'}
def _space(spec: dict, title: str) -> dict | None:
"""
فضای سه‌بعدی با تصویرِ مایل (ثابت، نه چرخان). مثلِ plane: آیتمِ خراب حذف می‌شود، شکلِ بی‌آیتم None است.
مختصات در بازه‌ی ±۱۰۰ و فقط عدد؛ هیچ متنِ آزادی جز برچسب‌های کوتاه نمی‌گذرد.
"""
points = {'O': [0.0, 0.0, 0.0]}
raw = spec.get('points') or {}
if not isinstance(raw, dict):
return None
for name, value in list(raw.items())[:MAX_POINTS]:
if not isinstance(name, str) or not _NAME_OK.match(name):
continue
try:
if not (isinstance(value, (list, tuple)) and len(value) == 3):
raise ValueError('triple')
points[name] = [_num(v, -100, 100) for v in value]
except ValueError:
continue
items = []
for it in (spec.get('items') or [])[:MAX_ITEMS + 8]:
if not isinstance(it, dict):
continue
try:
kind = it.get('type')
out = {'type': kind}
if it.get('color') in _SPACE_COLORS:
out['color'] = it['color']
if it.get('label'):
out['label'] = _label(it['label'], 24)
if kind in ('point', 'drop'):
if it.get('point') not in points:
continue
out['point'] = it['point']
elif kind in ('vector', 'segment'):
if it.get('from') not in points or it.get('to') not in points:
continue
out['from'], out['to'] = it['from'], it['to']
if it.get('dashed'):
out['dashed'] = True
elif kind == 'polygon':
names = it.get('points')
if not (isinstance(names, list) and 3 <= len(names) <= 6 and all(n in points for n in names)):
continue
out['points'] = list(names)
elif kind == 'box':
to = it.get('to')
if not (isinstance(to, (list, tuple)) and len(to) == 3):
continue
out['to'] = [_num(v, -100, 100) for v in to]
else:
continue
items.append(out)
except ValueError:
continue
if not items and len(points) < 2:
return None
return {'kind': 'space', 'title': title, 'axes': spec.get('axes') is not False, 'points': points, 'items': items[:MAX_ITEMS + 8]}
def validate(spec) -> dict | None: def validate(spec) -> dict | None:
""" """
نسخه‌ی تمیزِ spec، یا None. فقط کلیدها و نوع‌های شناخته‌شده رد می‌شوند؛ نسخه‌ی تمیزِ spec، یا None. فقط کلیدها و نوع‌های شناخته‌شده رد می‌شوند؛
@@ -767,6 +838,8 @@ def validate(spec) -> dict | None:
return _plane(spec, title) return _plane(spec, title)
if kind == 'numberline': if kind == 'numberline':
return _numberline(spec, title) return _numberline(spec, title)
if kind == 'space':
return _space(spec, title)
if kind == 'fraction': if kind == 'fraction':
shape = spec.get('shape') if spec.get('shape') in ('circle', 'rect') else 'circle' shape = spec.get('shape') if spec.get('shape') in ('circle', 'rect') else 'circle'
compare = [] compare = []

View File

@@ -461,3 +461,29 @@ class FifthRoundTests(SimpleTestCase):
self.assertEqual((item['type'], item['shape'], item['w'], item['top'], item['h']), ('quad', 'trapezoid', 10.0, 6.0, 4.0)) self.assertEqual((item['type'], item['shape'], item['w'], item['top'], item['h']), ('quad', 'trapezoid', 10.0, 6.0, 4.0))
item = parse('{"kind":"rectangle","length":8,"width":5}')['items'][0] item = parse('{"kind":"rectangle","length":8,"width":5}')['items'][0]
self.assertEqual((item['w'], item['h']), (8.0, 5.0)) self.assertEqual((item['w'], item['h']), (8.0, 5.0))
class SpaceDiagramTests(SimpleTestCase):
"""kind=space: فضای سه‌بعدیِ ثابت؛ ورودیِ خراب حذف می‌شود و چیزی از بیرون به SVG نمی‌رسد."""
def test_valid_space_keeps_points_and_items(self):
spec = {'kind': 'space', 'title': 'بردار', 'points': {'A': [1, 2, 3]},
'items': [{'type': 'vector', 'from': 'O', 'to': 'A', 'label': 'a', 'color': 'blue'},
{'type': 'drop', 'point': 'A'}, {'type': 'box', 'to': [1, 2, 3]}]}
out = validate(spec)
self.assertEqual(out['kind'], 'space')
self.assertEqual(out['points']['O'], [0.0, 0.0, 0.0])
self.assertEqual([i['type'] for i in out['items']], ['vector', 'drop', 'box'])
def test_bad_items_are_dropped_not_trusted(self):
out = validate({'kind': 'space', 'points': {'A': [1, 2, 3], 'B': [1, 2], 'C': [1e9, 0, 0]},
'items': [{'type': 'vector', 'from': 'A', 'to': 'B'}, # B نامعتبر
{'type': 'script', 'src': 'x'}, # نوعِ ناشناخته
{'type': 'segment', 'from': 'O', 'to': 'A', 'color': '<b>'},
{'type': 'polygon', 'points': ['A', 'O']}]}) # کمتر از سه نقطه
self.assertEqual(out['items'], [{'type': 'segment', 'from': 'O', 'to': 'A'}])
self.assertNotIn('B', out['points'])
self.assertNotIn('C', out['points'])
def test_empty_space_is_rejected(self):
self.assertIsNone(validate({'kind': 'space', 'points': {}, 'items': []}))